/* ============================================================
   COMPONENTS — the small vocabulary this app is built from.
   Class names are `ssb-` prefixed so nothing collides with anything
   a browser extension injects into a kiosk page.

   How things move here, so it does not have to be re-decided:

     - A control acknowledges a press at once: scale to --press-scale
       on :active, over --dur-press. That is the only motion most of
       this app has.
     - :hover is always inside `(hover: hover) and (pointer: fine)`.
     - Only transform and opacity move. Colour may fade. Nothing
       animates a size, a position or a border width.
     - Emphasis is an inset shadow, never a thicker border, so a marked
       cell is the same size as its neighbours and the grid stays
       straight.
     - Switching day, week, month or space is instant. It is done many
       times a visit and an animation there is a delay, not a nicety.
   ============================================================ */

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

.ssb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-body);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-press) var(--ease-out);
}
.ssb-btn:active:not(:disabled) { transform: scale(var(--press-scale)); }
.ssb-btn:disabled { opacity: 0.45; cursor: not-allowed; }

.ssb-btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.ssb-btn--danger { color: var(--danger-text); border-color: var(--danger-line); }

.ssb-btn--ghost { border-color: transparent; background: transparent; }
/* A ghost button answers a press with a fill as well as the scale: on a tinted
   block the scale alone is too small to see. */
.ssb-btn--ghost:active:not(:disabled) { background: var(--surface-press); }
/* A quiet button that blocks, removes or switches something off: no box, but
   the danger ink in its words and icon, so it is not read as a neutral step.
   Pressed, it takes the danger fill. */
.ssb-btn--ghost.ssb-btn--danger { color: var(--danger-text); }
.ssb-btn--ghost.ssb-btn--danger:active:not(:disabled) { background: var(--danger-fill); }

@media (hover: hover) and (pointer: fine) {
  .ssb-btn:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--ink-400); }
  .ssb-btn--primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
  .ssb-btn--danger:hover:not(:disabled) { background: var(--danger-fill); border-color: var(--danger-line); }
  .ssb-btn--ghost:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--line); }
  .ssb-btn--ghost.ssb-btn--danger:hover:not(:disabled) { background: var(--danger-fill); border-color: var(--danger-line); }
}

.ssb-btn--block { width: 100%; }
/* A label that may be longer than its button is allowed to grow. */
.ssb-btn__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ssb-btn--lg { min-height: var(--touch-kiosk); font-size: var(--text-lg); padding: var(--space-3) var(--space-5); }

/* ---------- Segmented control (Day / Week / Month) ---------
   A pill behind the chosen segment. On the phone it slides to a new one
   (Ivan, 2026-10-17: "let the pill slide on the phone, quickly"): 200ms,
   a transform only, only when a pointer chose. At rest the chosen segment
   draws its own pill; ui.js lays a stand-in over it for the slide and takes
   it away after. The words change at once, so what is chosen never waits on
   the motion. The wall screen has its own buttons and stays instant. */

/* A grid of auto columns, not a row of flex items: when the words do not
   all fit, every segment starts from its longest word and the room left is
   shared out until each has its whole label. So a short label ("It starts")
   gets all of its width first and stays on one line, and only the longer
   ones break between words. A row of flex items shrinks every one by the
   same share, and breaks them all. */
.ssb-segmented {
  position: relative;
  display: inline-grid;
  grid-auto-flow: column;
  max-width: 100%;
  padding: 3px;
  gap: 2px;
  background: var(--surface-sunk);
  border-radius: var(--radius);
}
/* The chosen segment, and the stand-in that slides to it, are one look: a
   solid accent segment with its word bold in the ink for a solid fill, so
   which view is on is seen without reading the view (Ivan, 2026-10-19:
   "extremely unclear until you read the content"; 2026-10-20, of a chosen
   toggle: "Solid is the best"). */
.ssb-segmented__pill,
.ssb-segmented button[aria-pressed="true"] {
  background: var(--accent);
  box-shadow: var(--shadow-raised);
}
/* transform-origin left, so its stretch reads from the segment it left. */
.ssb-segmented__pill {
  position: absolute;
  border-radius: var(--radius-sm);
  transform-origin: left;
  pointer-events: none;
}
.ssb-segmented button {
  position: relative;
  border: 0;
  background: transparent;
  /* Body ink, not muted: muted grey on the sunk track is under 4.5:1. */
  color: var(--text-body);
  padding: var(--space-2) var(--space-4);
  min-height: 36px;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out);
}
.ssb-segmented button:active { transform: scale(var(--press-scale)); }
.ssb-segmented button[aria-pressed="true"] {
  color: var(--on-accent);
  font-weight: var(--fw-bold);
}
/* Every word keeps room for its bold, so choosing one does not nudge its
   neighbours sideways. */
.ssb-segmented button::after {
  content: attr(data-label);
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: var(--fw-bold);
}
/* While the stand-in slides in, the segment's own pill waits under it. */
.ssb-segmented button[data-sliding] { background: transparent; box-shadow: none; }
@media (hover: hover) and (pointer: fine) {
  .ssb-segmented button:hover { color: var(--text-strong); }
}
/* Compact under a mouse, full height under a finger. */
@media (pointer: coarse) {
  .ssb-segmented button { min-height: var(--touch-min); }
}

/* ---------- Switch (a rule that can be on or off) -----------
   As on a phone's settings: a track of one width, the knob sliding from
   one end to the other, and the word on the side the knob has left. On and
   off differ in fill, position and text — not just in hue. The knob moves
   by transform; the fill fades. Under reduced motion the knob is simply at
   its end. */

.ssb-switch {
  --switch-w: 4.5rem;
  --knob: 24px;
  position: relative;
  display: inline-flex;
  align-items: center;
  /* Its width is what the knob's travel is measured against: beside a long
     sentence in a flex row it was squeezed to half and clipped its "On". */
  flex-shrink: 0;
  width: var(--switch-w);
  min-height: 32px;
  padding: 3px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  cursor: pointer;
  transition:
    transform var(--dur-press) var(--ease-out),
    background-color var(--dur-base) var(--ease),
    border-color var(--dur-base) var(--ease);
}
.ssb-switch:active { transform: scale(var(--press-scale)); filter: brightness(0.9); }
.ssb-switch__knob {
  flex-shrink: 0;
  width: var(--knob);
  height: var(--knob);
  border-radius: 50%;
  background: var(--ink-400);
  transition: transform var(--dur-base) var(--ease-out);
}
/* The word sits in the half the knob is not in. */
.ssb-switch__word {
  position: absolute;
  top: 0;
  bottom: 0;
  right: var(--space-3);
  display: flex;
  align-items: center;
}
.ssb-switch[aria-checked="true"] .ssb-switch__word { right: auto; left: var(--space-3); }
.ssb-switch[aria-checked="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.ssb-switch[aria-checked="true"] .ssb-switch__knob {
  background: var(--on-accent);
  /* The whole track less the knob, its padding and its border. */
  transform: translateX(calc(var(--switch-w) - var(--knob) - 8px));
}
/* Locked: it cannot be turned on, and the sentence beside it says why. Sunk,
   with a faint edge, and no press: it is not something to do. */
.ssb-switch:disabled {
  background: var(--surface-sunk);
  border-color: var(--line);
  cursor: not-allowed;
}
.ssb-switch:disabled:active { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .ssb-switch__knob { transition: none; }
}
@media (hover: hover) and (pointer: fine) {
  .ssb-switch:not(:disabled):hover { border-color: var(--ink-400); }
  .ssb-switch[aria-checked="true"]:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
}
/* Compact under a mouse, a full target under a finger. */
@media (pointer: coarse) {
  .ssb-switch { min-height: var(--touch-min); }
}

/* ---------- Slot row (the resident day view) ---------------
   One line per window: time leads, window name second, state right.
   Time is tabular so the column reads straight down.

   The window's length sits with its time — the time says when, the
   length says how much — so a row states what it contains before it is
   opened. Stacked under the time on a phone, where height is free and
   width is not; the wall screen sets the two side by side (kiosk.css). */

.ssb-slots { border-top: 1px solid var(--line); }

.ssb-slot {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-3);
  border: 0;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid transparent;
  /* The state colours its own end of the row: its fill lies under the
     word and icon and fades out towards the time (Ivan, 2026-10-19), so
     the row and its state read as one thing. The word and icon still say
     it; the wash is never the only sign. */
  --wash: transparent;
  background-color: transparent;
  /* Solid under the state's end, then fading out towards the time. */
  background-image: linear-gradient(to left, var(--wash) 0 var(--cap-width), transparent 75%);
  --cap-width: 5.5rem;
  text-align: left;
  cursor: pointer;
  /* When the three parts cannot share a line, the state drops to a line
     of its own rather than being pushed off the row. */
  flex-wrap: wrap;
  transition: background-color var(--dur-fast) var(--ease);
}
.ssb-slot--free  { --wash: var(--free-fill); }
.ssb-slot--taken { --wash: var(--taken-fill); }
/* Muted grey loses contrast where the wash lies under it. */
.ssb-slot:is(.ssb-slot--free, .ssb-slot--taken, .ssb-slot--closed) .ssb-slot__name { color: var(--text-body); }
/* The plate is the same grey as the taken wash: its edge keeps it a plate. */
.ssb-slot--taken .ssb-apt { box-shadow: inset 0 0 0 1px var(--taken-line); }
/* A full-width row does not shrink when pressed — at this width a scale
   reads as the list jumping. It darkens instead. */
.ssb-slot:active:not([disabled]) { background-color: var(--surface-press); }
.ssb-slot[disabled] { cursor: default; }
/* Rows sit flush inside a clipped card, so the ring goes inside. */
.ssb-slot { --ring-gap: calc(-1 * var(--ring-width)); }
@media (hover: hover) and (pointer: fine) {
  .ssb-slot:hover:not([disabled]) { background-color: var(--surface-hover); }
}

.ssb-slot__when {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  min-width: 6.5rem;
}
/* A time range and a length are each one thing. Neither is ever broken
   across lines. */
.ssb-slot__time {
  font-size: var(--text-md);
  line-height: var(--lh-snug);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text-strong);
}
.ssb-slot__length {
  font-size: var(--text-xs);
  line-height: var(--lh-snug);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text-muted);
}
/* The window name is the part that yields: it wraps between words, and only
   a word longer than the whole line is broken, with a hyphen where the
   browser can place one. It keeps a floor, so that beside a long apartment
   label it stays readable and the label moves down instead. */
.ssb-slot__name {
  flex: 1 1 5rem;
  min-width: 0;
  overflow-wrap: break-word;
  hyphens: auto;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.ssb-slot__state {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 100%;
  margin-left: auto;
  text-align: right;
}

/* Over is lighter, not faded (STYLE.md): muted ink and regular weight, so
   "Finished" can still be read. */
.ssb-slot--past .ssb-slot__time { color: var(--text-muted); }
.ssb-slot--past .ssb-slot__state { color: var(--text-muted); font-size: var(--text-xs); }

/* The resident's own booking. A left bar rather than a full outline:
   it marks the row without boxing it in, so a week of your own slots
   doesn't turn into a wall of rectangles. */
.ssb-slot--mine {
  border-left-color: var(--mine-line);
  background-color: var(--mine-fill);
  /* Yours is the whole row's tint, deepening under "Yours". */
  --wash: color-mix(in oklab, var(--mine-line) 22%, var(--mine-fill));
}
.ssb-slot--mine .ssb-slot__time { color: var(--mine-text); font-weight: var(--fw-medium); }
.ssb-slot--mine .ssb-slot__name,
.ssb-slot--mine .ssb-slot__length { color: var(--mine-text); }

.ssb-slot--closed { --wash: var(--closed-fill); }
/* A closure's reason is the caretaker's own sentence and may be long: it
   sits under the window's name and wraps there. */
.ssb-slot__reason {
  display: block;
  margin-top: 2px;
  font-size: var(--text-xs);
  color: var(--closed-text);
}

/* The state's end of the row (calendar.js, renderDay). The icon sits in a
   disc of the state's own line colour, drawn in the ink that goes on a
   solid fill, over the word in the state's ink; the column is the solid
   part of the wash. Together they read as the row's end, not as a word
   printed on top of it (Ivan, 2026-10-20: "text plastered on"). */
.ssb-slot__cap {
  flex-direction: column;
  justify-content: center;
  /* Held tight to its word, so disc and word read as one mark (Ivan). */
  gap: 1px;
  line-height: var(--lh-tight);
  min-width: var(--cap-width);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-tight);
}
.ssb-slot__disc {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--disc, currentColor);
  color: var(--on-accent);
}
/* One glyph size in every disc, centred on both axes whatever its path. */
.ssb-slot__disc .ssb-icon { display: block; width: 14px; height: 14px; }
.ssb-state--free .ssb-slot__disc   { --disc: var(--free-line); }
.ssb-state--mine .ssb-slot__disc   { --disc: var(--mine-line); }
.ssb-state--closed .ssb-slot__disc { --disc: var(--closed-line); }
/* The cap fills the row's height, so the wash under it is one block. */
.ssb-slot__state:has(> .ssb-slot__cap) {
  align-self: stretch;
  margin-block: calc(-1 * var(--space-2));
}
/* An overnight range: its second day on a line of its own, then the moon
   that says it runs past midnight. */
.ssb-slot__until { display: block; }
.ssb-slot__time .ssb-icon { margin-left: var(--space-1); vertical-align: -2px; color: var(--text-muted); }

/* ---------- Apartment label --------------------------------
   The one thing anyone learns about another household. */
.ssb-apt {
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-plate);
  /* A flat plate, not an outlined one: an outline means you can act on it,
     and nobody can act on another household. */
  background: var(--taken-fill);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--fw-heavy);
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  /* An apartment label is an identifier. It is never broken across
     lines — "王秀 / 英" is not the same thing as "王秀英". */
  white-space: nowrap;
}

/* ---------- State word --------------------------------------
   What a slot is, as a word. Not a chip: a column of tinted, outlined
   lozenges down the right edge is the loudest thing on the screen and
   says nothing the word does not. The word carries it — heavier than
   anything else in the row, in the ink of its state — with its icon in
   front, so free, yours and closed differ in shape as well as colour.
   Another household's plate has no icon. */
.ssb-state {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
  white-space: nowrap;
  font-size: var(--text-sm);
  font-weight: var(--fw-bold);
}
.ssb-state--free   { color: var(--free-text); }
.ssb-state--mine   { color: var(--mine-text); }
.ssb-state--closed { color: var(--closed-text); }
.ssb-state--taken  { color: var(--taken-text); }
/* Free, but not to this household: a plain word, no tick, no green. */
.ssb-state--cant   { color: var(--text-body); font-weight: var(--fw-regular); }

/* ---------- Slot cell (the grid tile) -----------------------
   Shared by the resident week view and the wall screen, so it lives
   here rather than in kiosk.css. The kiosk only overrides its sizing.
   State is carried by fill AND by the word printed on it — never by hue
   alone, because this gets read at three metres, under a fluorescent
   tube, by people who may not distinguish green from grey.

   Every window keeps its tile and its frame (Ivan, 2026-10-19: "keep at
   least the card frame ... but keep it greyed out"). Only a tile somebody
   can act on has a coloured edge and a wash: a free one, and your own.
   Taken, closed, can't-book and finished have a grey hairline and a flat
   fill, faded; so on a week that is mostly behind you the eye still goes
   to the few coloured tiles, which are the ones to tap. */

.ssb-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: var(--space-2);
  /* Always there, drawn only where it means something, so an outlined
     tile is the same size as the flat one beside it. */
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  text-align: center;
  cursor: pointer;
  min-height: var(--touch-min);
  min-width: 0;
  transition: transform var(--dur-press) var(--ease-out);
}
.ssb-cell:active:not([disabled]) { transform: scale(var(--press-scale)); }
.ssb-cell[disabled] { cursor: default; }
.ssb-cell__time  { font-size: var(--text-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.ssb-cell__state {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  max-width: 100%;
  /* A tile is too narrow to keep a long label on one line, so here it
     may wrap — but it stays inside its own tile. */
  overflow-wrap: anywhere;
  font-size: var(--text-sm);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
}
/* Set by calendar.js when the word is a long apartment label. */
.ssb-cell[data-long] .ssb-cell__state { font-size: var(--text-xs); }

/* The state's fill lies under its word, at the top, and fades down the
   tile: the same wash as a day row, turned to the way a tile is read. */
.ssb-cell--free {
  background: linear-gradient(to bottom, var(--free-fill) 35%, color-mix(in oklab, var(--free-fill) 35%, var(--surface)));
  border-color: var(--free-line);
}
.ssb-cell--free .ssb-cell__state { color: var(--free-text); }

/* Faded, not gone: somebody else has it, so it is there to be read and
   not to be reached for — a pale fill and a regular weight beside the
   bold words on the tiles that can be booked. The wall screen keeps its
   own bold (kiosk.css): there the word is read from three metres. */
.ssb-cell--taken  { background: var(--taken-fill); border-color: var(--taken-line); opacity: var(--taken-fade); }
.ssb-cell--taken .ssb-cell__state { color: var(--taken-text); font-weight: var(--fw-regular); }

/* The heavier edge is an inset shadow, not a wider border: a 2px border
   makes this one tile 2px bigger than every tile beside it. */
.ssb-cell--mine {
  --mine-edge: 1px;
  background: linear-gradient(to bottom, color-mix(in oklab, var(--mine-line) 22%, var(--mine-fill)) 35%, var(--mine-fill));
  border-color: var(--mine-line);
  box-shadow: inset 0 0 0 var(--mine-edge) var(--mine-line);
}
.ssb-cell--mine .ssb-cell__state { color: var(--mine-text); }

/* Free, but not to this household (calendar.js, cantBook): no fill and no
   coloured edge, because those say "you can act on this", and the word in
   plain ink. A grey frame says a window is there. It still opens the
   sheet, which says why. */
.ssb-cell--cant { border-color: var(--line-strong); }
.ssb-cell--cant .ssb-cell__state { color: var(--text-body); font-weight: var(--fw-regular); }

.ssb-cell--closed { background: var(--closed-fill); border-color: var(--closed-line); }
.ssb-cell--closed .ssb-cell__state { color: var(--closed-text); font-size: var(--text-xs); }

/* Been and gone: the frame stays, grey, so the week keeps its shape. */
.ssb-cell--past   { border-color: var(--line-strong); }
.ssb-cell--past .ssb-cell__time { color: var(--text-muted); }

/* ---------- The week calendar --------------------------------
   Phone and computer (calendar.js, renderWeekGrid). Seven columns ruled
   apart, the way a paper week is; a row is the windows that start at the
   same time, and every tile in it is the same height. The rules are the
   grid's own colour showing through a 1px gap between columns, so they
   run the full height whether or not a day has anything in it. */

.ssb-tgrid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  /* The header is its own height; every row of tiles is as tall as the
     tallest, so the week stays level whatever the data holds. */
  grid-template-rows: auto;
  grid-auto-rows: 1fr;
  column-gap: 1px;
  container-type: inline-size;
  background: var(--line);
  background-clip: content-box;
}
.ssb-tgrid > * { background: var(--surface); }
.ssb-tgrid__day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding-bottom: var(--space-2);
}
.ssb-tgrid__dow { font-size: var(--text-xs); color: var(--text-muted); }
/* Today says so in words as well as by its mark. */
.ssb-tgrid__dow--today { color: var(--text-strong); font-weight: var(--fw-bold); }
.ssb-tgrid__slot { display: flex; padding: 2px; }
.ssb-tgrid .ssb-cell { flex: 1; padding: var(--space-2) 0; }
/* Seven columns on a phone leave about 40px for a word: at the smallest
   size it holds "Closed" and a three-character apartment whole, and a word
   is never broken in the middle — "Free to take" wraps at its spaces. */
.ssb-tgrid .ssb-cell__state {
  font-size: var(--text-xs);
  overflow-wrap: normal;
}
/* Another household's label is the one word allowed to break anywhere: it
   has no other way into a seventh of a phone. Two lines at most, so one
   long name cannot make every row tall; the sheet says it whole. */
.ssb-tgrid .ssb-cell--taken .ssb-cell__state {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.ssb-tgrid .ssb-cell__time { font-size: var(--text-xs); line-height: var(--lh-tight); }
/* A computer's columns are three times a phone's: the word that tells the
   states apart gets the room back. */
@container (min-width: 30rem) {
  .ssb-tgrid .ssb-cell__state { font-size: var(--text-sm); }
  .ssb-tgrid .ssb-cell { padding: var(--space-3) 0; }
}

/* ---------- Cards, sheets ----------------------------------- */

.ssb-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}

/* The sheet arrives and leaves by transition, not keyframe: it can be
   dismissed while still arriving, and a transition turns round where a
   keyframe would restart.

   On a phone it is a drawer — it comes up from the bottom edge by its
   own height and goes back the same way. From 640px it is a centred
   dialog, which has no edge to come from, so it scales up slightly
   instead. Leaving is quicker than arriving, in both.

   The dim is a pseudo-element so it can fade on its own; fading the
   backdrop itself would fade the sheet inside it.

   What came up from the bottom can be pushed back down: under a finger
   the drawer follows it, and the dim lifts as it goes, which is what
   says letting go here will close it. openSheet() tracks the finger;
   everything after it lifts is the same enter and exit. */

.ssb-sheet-backdrop {
  /* How far the drawer has been dragged and how much dim is left; at rest
     until openSheet() sets them from a touch. */
  --sheet-drag: 0px;
  --sheet-dim: 1;

  position: fixed;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: var(--z-sheet);
}
.ssb-sheet-backdrop::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--scrim);
  transition: opacity var(--dur-base) var(--ease);
}
@media (min-width: 640px) { .ssb-sheet-backdrop { align-items: center; } }

.ssb-sheet {
  --sheet-from: translateY(100%);
  --sheet-from-opacity: 1;
  --sheet-dur: var(--dur-sheet);
  --sheet-ease: var(--ease-drawer);
  /* Read by openSheet(): 1 where the sheet is a drawer and can be dragged. */
  --sheet-swipe: 1;

  position: relative;
  width: 100%;
  max-width: 32rem;
  max-height: 90dvh;
  overflow-y: auto;
  /* Reaching the end of the sheet must not start scrolling the page
     behind it. */
  overscroll-behavior: contain;
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: var(--space-4) var(--space-4) calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  transition:
    transform var(--sheet-dur) var(--sheet-ease),
    opacity var(--sheet-dur) var(--sheet-ease);
}
/* The handle: the only thing that says the drawer can be pulled. It sits
   in the top padding, and only where there is a finger to pull with. */
@media (pointer: coarse) {
  .ssb-sheet::before {
    content: "";
    position: absolute;
    top: var(--space-2);
    left: 50%;
    width: var(--space-6);
    height: var(--space-1);
    margin-left: calc(var(--space-6) / -2);
    border-radius: var(--radius-sm);
    background: var(--line-strong);
  }
}
@media (min-width: 640px) {
  .ssb-sheet {
    --sheet-from: scale(0.96);
    --sheet-from-opacity: 0;
    --sheet-dur: var(--dur-base);
    --sheet-ease: var(--ease-out);
    --sheet-swipe: 0;
    border-radius: var(--radius-lg);
    padding-bottom: var(--space-5);
  }
  .ssb-sheet::before { content: none; }
}
/* Reduced motion: the sheet fades and nothing travels. A finger may still
   move it — that is the person, not an animation — but let go, it is back
   at once, or fades out where it was left. */
@media (prefers-reduced-motion: reduce) {
  .ssb-sheet {
    --sheet-from: translateY(var(--sheet-drag));
    --sheet-from-opacity: 0;
    --sheet-dur: var(--dur-base);
    --sheet-ease: var(--ease);
    transition-property: opacity;
  }
}

@starting-style {
  .ssb-sheet-backdrop::before { opacity: 0; }
  .ssb-sheet { transform: var(--sheet-from); opacity: var(--sheet-from-opacity); }
}

/* Set by openSheet() while a finger holds the drawer. No transition: it
   has to be under the finger, not catching up with it. */
.ssb-sheet-backdrop[data-dragging]::before { opacity: var(--sheet-dim); transition: none; }
.ssb-sheet-backdrop[data-dragging] .ssb-sheet { transform: translateY(var(--sheet-drag)); transition: none; }
/* Set by openSheet() while a released drawer settles on its spring, which
   is an animation: the sheet's own transition would win over it. */
.ssb-sheet-backdrop[data-settling] .ssb-sheet { transition: none; }

/* Set by openSheet() while the sheet leaves; the node is removed when
   the transition ends. Nothing in a leaving sheet can be clicked. */
.ssb-sheet-backdrop[data-closing] { pointer-events: none; }
.ssb-sheet-backdrop[data-closing]::before { opacity: 0; transition-duration: var(--dur-exit); }
.ssb-sheet-backdrop[data-closing] .ssb-sheet {
  transform: var(--sheet-from);
  opacity: var(--sheet-from-opacity);
  transition-duration: var(--dur-exit);
  transition-timing-function: var(--ease-out);
}

/* The side drawer: from 640px a panel that comes in from the right edge,
   full height, and goes back to it. Below that it is the ordinary drawer
   from the bottom. openSheet(…, {side: true}). */
@media (min-width: 640px) {
  .ssb-sheet-backdrop--side { align-items: stretch; justify-content: flex-end; }
  .ssb-sheet-backdrop--side .ssb-sheet {
    --sheet-from: translateX(100%);
    --sheet-from-opacity: 1;
    --sheet-dur: var(--dur-sheet);
    --sheet-ease: var(--ease-drawer);
    max-width: 30rem;
    max-height: none;
    height: 100dvh;
    border-radius: 0;
    border-left: 1px solid var(--line);
  }
}
/* Reduced motion: it fades in where it ends up, and nothing travels. */
@media (min-width: 640px) and (prefers-reduced-motion: reduce) {
  .ssb-sheet-backdrop--side .ssb-sheet { --sheet-from: none; --sheet-from-opacity: 0; }
}

/* ---------- The address plate --------------------------------
   A web address residents are given, drawn as the plate by a door: enamel
   when it is in use, dashed when it is asked for and not real yet, or has
   stopped working. The edge is 2px in every state, so a plate that goes
   live does not grow. Copy and Open are joined to it, edge to edge; they
   darken on press rather than scale, because they are as wide as it is. */
.ssb-plate {
  display: grid;
  border: 2px solid var(--plate-ground);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.ssb-plate__face {
  background: var(--plate-ground);
  color: var(--plate-ink);
  padding: var(--space-4) var(--space-4) var(--space-3);
  /* The engraved line inside the edge. */
  box-shadow: inset 0 0 0 3px var(--plate-ground), inset 0 0 0 4px color-mix(in srgb, var(--plate-ink) 35%, transparent);
}
.ssb-plate__kicker { margin: 0 0 var(--space-1); font-size: var(--text-xs); font-weight: var(--fw-bold); }
.ssb-plate__host {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  line-height: var(--lh-tight);
  overflow-wrap: anywhere;
}
.ssb-plate__host b { font-weight: var(--fw-heavy); }
.ssb-plate__state { margin-top: var(--space-2); }
.ssb-plate__state .ssb-state { color: inherit; }
.ssb-plate__acts { display: grid; grid-template-columns: 1fr 1fr; background: var(--surface); }
.ssb-plate__acts > .ssb-btn {
  border: 0;
  border-radius: 0;
  justify-content: center;
  min-height: var(--touch-min);
}
.ssb-plate__acts > .ssb-btn:focus-visible { outline-offset: -4px; }
.ssb-plate__acts > .ssb-btn:active { transform: none; background: var(--surface-press); }
.ssb-plate__acts > .ssb-btn + .ssb-btn { border-left: 2px solid var(--plate-ground); }
.ssb-plate--dashed { border-style: dashed; border-color: var(--line-strong); }
.ssb-plate--dashed .ssb-plate__face { background: transparent; color: var(--text-strong); box-shadow: none; }
.ssb-plate--small .ssb-plate__host { font-size: var(--text-lg); }
.ssb-plate--small .ssb-plate__face { padding: var(--space-3); }

/* ---------- The track: Asked, Records right, On ----------------
   Three marks on a line that is drawn to the one reached. Not buttons:
   nobody moves along it but the building's DNS and Maretou. */
.ssb-track {
  --reached: 0;
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin: 0;
  padding: 0;
  list-style: none;
}
.ssb-track::before,
.ssb-track::after {
  content: "";
  position: absolute;
  top: 13px;
  left: calc(100% / 6);
  height: 3px;
}
.ssb-track::before { right: calc(100% / 6); background: var(--line); }
.ssb-track::after { width: calc(var(--reached) * 100% / 3); background: var(--ok-line); }
.ssb-track__pt {
  position: relative;
  display: grid;
  justify-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.ssb-track__dot {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 2px solid var(--line-strong);
  border-radius: 50%;
  background: var(--surface);
  font-size: var(--text-xs);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.ssb-track__pt--done .ssb-track__dot { background: var(--ok-line); border-color: var(--ok-line); color: var(--on-accent); }
.ssb-track__pt--now { color: var(--text-strong); font-weight: var(--fw-heavy); }
.ssb-track__pt--now .ssb-track__dot { box-shadow: 0 0 0 4px var(--ok-fill); }

/* ---------- Records to add at a DNS provider -------------------
   Ruled, not boxed, on a rail that joins the asked-for plate above to the
   Maretou one below. Each value in the mono face, so 0 and O, l and 1 are
   told apart, each with its own Copy. */
.ssb-records {
  display: grid;
  gap: var(--space-4);
  margin-left: var(--space-3);
  padding-left: var(--space-4);
  border-left: 3px solid var(--line-strong);
}
.ssb-records__where { margin: var(--space-1) 0 0; color: var(--text-muted); font-size: var(--text-sm); }
.ssb-record { display: grid; gap: var(--space-2); padding-top: var(--space-3); border-top: 1px solid var(--line); }
.ssb-record__head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); }
.ssb-record__title { font-weight: var(--fw-bold); color: var(--text-strong); }
.ssb-record__kv { display: grid; grid-template-columns: 3.25rem 1fr auto; gap: var(--space-2); align-items: center; margin: 0; }
.ssb-record__kv dt { color: var(--text-muted); font-size: var(--text-sm); }
.ssb-record__v { margin: 0; min-width: 0; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-strong); overflow-wrap: break-word; }
.ssb-record__found { display: flex; gap: var(--space-1); margin: 0; color: var(--danger-text); font-size: var(--text-sm); }

/* ---------- The own-address panel and Admin → Address --------- */
.ssb-own { display: grid; gap: var(--space-4); align-content: start; }
.ssb-own__head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); margin-bottom: var(--space-4); }
.ssb-own__title, .ssb-address-tab__title { margin: 0; font-family: var(--font-display); font-size: var(--text-xl); color: var(--text-strong); }
.ssb-own__lead { margin: 0; color: var(--text-body); }
.ssb-own__ask { display: grid; gap: var(--space-4); }
.ssb-own__check { display: flex; align-items: center; gap: var(--space-3); }
.ssb-own__check > .ssb-btn { flex: none; }
.ssb-own__checked { color: var(--text-muted); font-size: var(--text-sm); }
.ssb-own__row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.ssb-own__row--spread { justify-content: space-between; }
.ssb-own__danger { color: var(--danger-text); justify-self: start; }
.ssb-own__confirm { display: grid; gap: var(--space-3); }
.ssb-own__confirm p { margin: 0; }
.ssb-own__text {
  margin: 0;
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  white-space: pre-wrap;
  font-size: var(--text-sm);
}
.ssb-own__kept summary { cursor: pointer; color: var(--text-body); }
.ssb-own__kept[open] summary { margin-bottom: var(--space-3); }
.ssb-address-card { display: grid; gap: var(--space-3); justify-items: stretch; }
.ssb-address-card > .ssb-btn--ghost { justify-self: start; }
.ssb-address-tab { display: grid; gap: var(--space-4); padding: var(--space-4); }
.ssb-address-tab__cols { display: grid; gap: var(--space-5); }
.ssb-address-tab__side { display: grid; gap: var(--space-4); align-content: start; }
.ssb-address-tab__own { display: grid; gap: var(--space-3); align-content: start; }
.ssb-address-tab__sub { margin: 0; font-size: var(--text-lg); color: var(--text-strong); }
@media (min-width: 900px) {
  .ssb-address-tab__cols { grid-template-columns: 5fr 7fr; gap: var(--space-6); }
}

.ssb-kv {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--line);
  font-size: var(--text-sm);
}
.ssb-kv dt { color: var(--text-muted); }
.ssb-kv dd { margin: 0; color: var(--text-strong); font-weight: var(--fw-medium); text-align: right; }

/* ---------- Terms — what a slot gives you --------------------
   Detail rows, in the sheet and in the wall screen's confirm panel,
   placed before the button that takes the slot. The label keeps its
   width and the value wraps beside it; counts ("1 of 4") are tabular so
   two allowances line up under each other. */
.ssb-terms { margin: 0; }
.ssb-sheet .ssb-terms { margin-top: var(--space-4); }
.ssb-terms .ssb-kv dt { flex-shrink: 0; }
.ssb-terms .ssb-kv dd { font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

/* In the phone's sheet the facts are a grid of four (Ivan's pick, the
   split card): two across, each a label over its value, the value large
   enough to be the thing read. Ruled, not boxed — a line under the top
   row and between the columns, no outline round each. An odd one out
   takes the whole last row. */
.ssb-sheet .ssb-terms {
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.ssb-sheet .ssb-terms .ssb-kv {
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-3) var(--space-3) 0;
  border-top: 0;
}
.ssb-sheet .ssb-terms .ssb-kv:nth-child(even) { padding-left: var(--space-3); border-left: 1px solid var(--line); }
.ssb-sheet .ssb-terms .ssb-kv:nth-child(n + 3) { border-top: 1px solid var(--line); }
.ssb-sheet .ssb-terms .ssb-kv:last-child:nth-child(odd) { grid-column: 1 / -1; }
.ssb-sheet .ssb-terms .ssb-kv dt { font-size: var(--text-xs); }
.ssb-sheet .ssb-terms .ssb-kv dd {
  text-align: left;
  font-size: var(--text-lg);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
}

/* ---------- Rules line — a space's rules, under its name ----
   Short phrases with a dot between. Each phrase moves to the next line
   whole; the dot stays at the end of the line it finishes rather than
   starting the next. */
.ssb-rules {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
  line-height: var(--lh-normal);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.ssb-rules li:not(:last-child)::after {
  content: "·";
  margin-left: var(--space-2);
  color: var(--text-faint);
}

/* ---------- Notices — how a rule explains itself ------------ */

.ssb-notice {
  padding: var(--space-3);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-sunk);
  font-size: var(--text-sm);
  line-height: var(--lh-normal);
}
.ssb-notice--error   { background: var(--danger-fill); border-color: var(--danger-line); color: var(--danger-text); }
.ssb-notice--success { background: var(--ok-fill);        border-color: var(--ok-line);        color: var(--ok-text); }
.ssb-notice--warn    { background: var(--attention-fill); border-color: var(--attention-line); color: var(--attention-text); }

/* ---------- Month density grid ------------------------------
   Four segments per day, filled for free slots. Reads as a bar chart
   of availability across the month, which is the question the month
   view exists to answer. */

/* Ruled, not boxed: one line above each week, the way a printed calendar
   is drawn. Thirty-one outlined tiles is thirty-one things to look at
   before the one that matters. */
.ssb-month {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  container-type: inline-size;
}
.ssb-month__dow {
  text-align: center;
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  /* Muted, not faint: faint is 2.9:1 on the page and this is the one row
     that says which column is which. */
  color: var(--text-muted);
  padding-bottom: var(--space-1);
}
.ssb-month__day {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-1);
  /* Room for the number and the meter with air between them, and never
     less than the touch floor — which on the wall screen is the taller. */
  min-height: max(52px, var(--touch-min));
  padding: var(--space-1);
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    transform var(--dur-press) var(--ease-out);
}
.ssb-month__day:active { transform: scale(var(--press-scale)); }
@media (hover: hover) and (pointer: fine) {
  .ssb-month__day:hover { background: var(--surface-hover); }
}
/* The blanks before the 1st keep their rule, so the line runs unbroken. */
.ssb-month__day--empty { cursor: default; pointer-events: none; }
.ssb-month__day--outside { cursor: default; }
/* Finished days, and days with no schedule, are dimmed by their contents
   and not as a whole: the rule above a dimmed day would fade with it and
   the line across the week would stutter. Finished days stay tappable. */
.ssb-month__day--outside > * { opacity: 0.4; }
.ssb-month__day--done .ssb-month__num { color: var(--text-muted); }
/* Today is never dimmed, even at nine in the evening when every window of it
   is over: it is the one date the whole screen is anchored to. */
.ssb-month__day--today > * { opacity: 1; }

/* Nothing left, or nothing offered. Blocked out with a fill and a word,
   so it reads from across the room and without the colour. */
.ssb-month__day--full   { background: var(--taken-fill); }
.ssb-month__day--closed { background: var(--closed-fill); }

/* The date is what somebody looks for, so it is the largest thing in the
   cell. Today and the day being viewed are marked below, with every other
   place a date is drawn. */
.ssb-month__num {
  min-width: 1.4em;
  padding: 0 var(--space-1);
  border-radius: var(--radius-mark);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--fw-bold);
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.ssb-month__day--done .ssb-month__num { font-weight: var(--fw-regular); }

/* One dot per window, yours first, then the ones this household can book,
   then the rest as hollow rings. Shape as well as colour: filled is a
   window for you, hollow is one that is not. */
.ssb-month__dots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* Kept apart, so they can be counted at a glance (design-taste 17), and
     inside their own date, so one day's dots never run into the next's. */
  gap: 3px;
  min-height: var(--month-dot);
}
/* On the month, so a capsule beside a count is the same size as in a row. */
.ssb-month {
  --month-dot: 6px;
  --month-dot-stroke: 1.5px;
}
@container (min-width: 30rem) {
  .ssb-month__dots, .ssb-month__count { --month-dot: 9px; }
}
/* Too many windows for dots: the household's capsule, then the count. */
.ssb-month__count .ssb-month__word { white-space: normal; line-height: var(--lh-tight); }
.ssb-month__count {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 3px;
  margin-inline: calc(-1 * var(--space-1));
}
.ssb-month__dot {
  width: var(--month-dot);
  height: var(--month-dot);
  border-radius: 50%;
  box-sizing: border-box;
}
/* Yours is a capsule, not only a blue dot: shape as well as colour. */
.ssb-month__dot--mine { width: calc(var(--month-dot) * 1.5); border-radius: var(--radius-pill); background: var(--mine-line); }
.ssb-month__dot--open { background: var(--free-line); }
.ssb-month__dot--shut { border: var(--month-dot-stroke) solid var(--text-muted); }

/* A space with one window a day: the whole date is that window, a box in
   its state's colours with its word under the number, as a week tile is.
   The box sits inside the cell, so the rule above each week runs unbroken. */
.ssb-month--single .ssb-month__day:not(.ssb-month__day--empty) {
  justify-content: flex-start;
  padding: 3px 2px;
  /* "Can't book" takes two lines in a phone's seventh; every row keeps
     that height, so the weeks stay level. */
  min-height: max(60px, var(--touch-min));
}
.ssb-month__box {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--space-1) 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
}
.ssb-month--single .ssb-month__word {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 1px;
  white-space: normal;
  line-height: var(--lh-tight);
}
/* The number and, for a window that runs into the next morning, its moon. */
.ssb-month__top { display: flex; align-items: flex-start; justify-content: center; }
.ssb-month--single .ssb-month__num { padding: 0 1px; min-width: 0; }
.ssb-month__moon { display: flex; margin-top: 2px; color: var(--text-muted); }
.ssb-month__moon .ssb-icon { width: 10px; height: 10px; }
.ssb-month__day--free .ssb-month__box {
  background: linear-gradient(to bottom, color-mix(in oklab, var(--free-fill) 40%, var(--surface)), var(--free-fill) 70%);
  border-color: var(--free-line);
}
.ssb-month__day--mine .ssb-month__box {
  background: linear-gradient(to bottom, var(--mine-fill), color-mix(in oklab, var(--mine-line) 22%, var(--mine-fill)) 70%);
  border-color: var(--mine-line);
}
.ssb-month__day--taken .ssb-month__box  { background: var(--taken-fill); border-color: var(--taken-line); }
.ssb-month__day--closed .ssb-month__box { background: var(--closed-fill); border-color: var(--closed-line); }
.ssb-month__day--cant .ssb-month__box   { border-color: var(--line-strong); }
.ssb-month--single .ssb-month__day--closed { background: transparent; }
.ssb-month__day--free .ssb-month__word { color: var(--free-text); font-weight: var(--fw-bold); }
.ssb-month__day--mine .ssb-month__word { color: var(--mine-text); font-weight: var(--fw-bold); }
.ssb-month__day--taken .ssb-month__word,
.ssb-month__day--cant  .ssb-month__word { color: var(--text-body); }
/* The word is wider than the day's padded box on a 320px phone, so it
   uses the whole width of the cell. */
.ssb-month__word {
  margin-inline: calc(-1 * var(--space-1));
  overflow: hidden;
  text-align: center;
  white-space: nowrap;
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-tight);
}
.ssb-month__day--full   .ssb-month__word { color: var(--taken-text); }
.ssb-month__day--closed .ssb-month__word { color: var(--closed-text); }
.ssb-month__word--count { color: var(--free-text); font-weight: var(--fw-bold); }

/* ---------- The day under the month -------------------------
   calendar.js, dayPanel. One date's windows, drawn as the Day view draws
   them, under the month it was chosen from. It arrives (fades in and
   settles 12px) because it is new on the page; moving to another date is
   done many times a visit, so the rows change at once and only the notch,
   which says which date this is, slides to its column. */

.ssb-monthday {
  --column: 0;
  position: relative;
  /* Joined to the date it opens (design-taste 9): its edge and notch are the
     ring's accent at the ring's width, and the notch reaches the month. */
  --join: 2px;
  margin-top: 10px;
  border: var(--join) solid var(--accent);
  border-radius: var(--radius-lg);
  background: var(--surface);
  transition:
    opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}
@starting-style {
  .ssb-monthday { opacity: 0; transform: translateY(-12px); }
}
.ssb-monthday[data-instant] { transition: none; }
.ssb-monthday[data-closing] {
  opacity: 0;
  transform: translateY(-8px);
  transition-duration: var(--dur-exit);
}
/* A seventh of the month's grid wide, moved by whole sevenths: translateX's
   percentages are of the notch itself, so 100% is exactly one column. Inset
   by the month's own padding, so it lines up with the dates above it. */
.ssb-monthday__notch {
  --month-inset: var(--space-3);
  position: absolute;
  bottom: 100%;
  left: var(--month-inset);
  width: calc((100% - 2 * var(--month-inset)) / 7);
  height: 8px;
  transform: translateX(calc(var(--column) * 100%));
  transition: transform var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.ssb-monthday__notch::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 3px;
  width: 14px;
  height: 14px;
  border-left: var(--join) solid var(--accent);
  border-top: var(--join) solid var(--accent);
  background: var(--surface);
  transform: translateX(-50%) rotate(45deg);
}
.ssb-monthday__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
}
.ssb-monthday__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--fw-heavy);
  letter-spacing: var(--tracking-tight);
  color: var(--text-strong);
}
.ssb-monthday__body .ssb-notice { margin: 0 var(--space-3) var(--space-3); }
.ssb-monthday__body .ssb-slots { border-radius: 0 0 var(--radius-lg) var(--radius-lg); overflow: hidden; }
.ssb-monthday__body .ssb-slots > li:last-child .ssb-slot { border-bottom: 0; }

@media (prefers-reduced-motion: reduce) {
  /* Gentler, not none: it still fades in, but does not travel. */
  @starting-style { .ssb-monthday { transform: none; } }
  .ssb-monthday[data-instant] { transition: none; }
.ssb-monthday[data-closing] { transform: none; }
  .ssb-monthday__notch { transition: none; }
}

/* ---------- Loading -----------------------------------------
   Constant motion is linear. The spinner is quick on purpose: the same
   wait feels shorter behind a fast spinner than a slow one. */

.ssb-skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--surface-sunk) 25%, var(--line) 37%, var(--surface-sunk) 63%);
  background-size: 200% 100%;
  animation: ssb-shimmer 1.4s linear infinite;
}

/* A ring turning, its arc in the accent and longer than a quarter, and
   easing round rather than ticking: busy, and visibly so, from a glance. */
.ssb-spinner {
  width: 18px; height: 18px;
  border: 2.5px solid var(--line);
  border-top-color: var(--accent);
  border-right-color: var(--accent);
  border-radius: 50%;
  animation: ssb-spin 0.9s var(--ease-in-out) infinite;
}

/* ---------- Code boxes --------------------------------------
   Six boxes, one digit each, over one real input that is invisible but
   takes every tap, paste and key. The next box to fill is outlined in the
   accent; a refused code outlines them all in the danger ink. */
.ssb-code {
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--space-2);
}
.ssb-code__box {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-kiosk);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-strong);
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--fw-heavy);
  font-variant-numeric: tabular-nums;
}
.ssb-code__box[data-next] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.ssb-code[data-invalid] .ssb-code__box { border-color: var(--danger-edge); color: var(--danger-text); }
.ssb-code__input {
  position: absolute;
  inset: 0;
  width: 100%;
  margin: 0;
  border: 0;
  padding: 0;
  opacity: 0;
  /* 16px or more, or iOS zooms the page on focus. */
  font-size: var(--text-md);
  cursor: text;
}

/* ---------- Checkbox ----------------------------------------
   The native input lies over the box, invisible but still what is
   clicked, focused and read aloud. Checked is a filled box and a tick,
   not just a colour; the tick pops in by scale, and under reduced motion
   it fades in instead. */
.ssb-check {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  width: var(--icon-lg);
  height: var(--icon-lg);
}
.ssb-check__input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.ssb-check__box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--on-accent);
  pointer-events: none;
  transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.ssb-check__box .ssb-icon {
  opacity: 0;
  transform: scale(0.4);
  transition: transform var(--dur-base) var(--ease-out), opacity var(--dur-fast) var(--ease);
}
.ssb-check__input:checked + .ssb-check__box { background: var(--accent); border-color: var(--accent); }
/* Disabled reads as the disabled button does. */
.ssb-check__input:disabled { cursor: not-allowed; }
.ssb-check__input:disabled + .ssb-check__box { opacity: 0.45; }
.ssb-check__input:checked + .ssb-check__box .ssb-icon { opacity: 1; transform: none; }
/* The input is invisible, so the box wears its focus ring. */
.ssb-check__input:focus-visible + .ssb-check__box {
  outline: var(--ring-width) solid var(--ring-color);
  outline-offset: var(--ring-gap);
}
@media (hover: hover) and (pointer: fine) {
  .ssb-check__input:hover:not(:checked) + .ssb-check__box { border-color: var(--ink-400); }
}
@media (prefers-reduced-motion: reduce) {
  .ssb-check__box .ssb-icon { transform: none; }
}

/* Still loading, still visibly so — but it breathes rather than moves. */
@media (prefers-reduced-motion: reduce) {
  .ssb-skeleton { animation: ssb-pulse 2s ease-in-out infinite; background: var(--surface-sunk); }
  .ssb-spinner  { animation: ssb-pulse 1.2s ease-in-out infinite; }
}

.ssb-empty {
  padding: var(--space-6) var(--space-4);
  text-align: center;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* ---------- A row that scrolls sideways ---------------------
   Tabs or space buttons that may not fit. The right edge fades so the
   row visibly continues; the end padding lets the last item scroll clear
   of the fade. */
.ssb-scroll-x {
  display: flex;
  overflow-x: auto;
  scrollbar-width: none;
  padding-right: var(--space-5);
  -webkit-mask-image: linear-gradient(to right, black calc(100% - var(--space-5)), transparent);
  mask-image: linear-gradient(to right, black calc(100% - var(--space-5)), transparent);
}
.ssb-scroll-x > * { flex-shrink: 0; }
/* Kept up to date by ui.js's edgeFades(): each edge fades only while there is
   more that way, growing with how much more up to --fade-max, which is wide
   enough that the item cut there reads as going on rather than broken.
   Scrolled to an end, that end has no fade and needs no padding. */
.ssb-scroll-x[data-fades] {
  --fade-max: var(--space-7);
  --fade-start: 0px;
  --fade-end: 0px;
  padding-right: 0;
  -webkit-mask-image: linear-gradient(to right, transparent, black var(--fade-start), black calc(100% - var(--fade-end)), transparent);
  mask-image: linear-gradient(to right, transparent, black var(--fade-start), black calc(100% - var(--fade-end)), transparent);
}

/* ---------- Toast -------------------------------------------
   Comes up from the bottom edge and goes back to it. Transitions, not
   keyframes: several can arrive in quick succession. */

.ssb-toasts {
  position: fixed;
  left: 50%;
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: min(24rem, calc(100vw - 2 * var(--space-4)));
}
.ssb-toast {
  --toast-from: translateY(100%);
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  background: var(--inverse-surface);
  color: var(--inverse-text);
  font-size: var(--text-sm);
  transition:
    transform var(--dur-base) var(--ease-out),
    opacity var(--dur-base) var(--ease-out);
}
.ssb-toast--error { background: var(--danger-solid); color: var(--on-danger); }
.ssb-toast__text { flex: 1; min-width: 0; }
/* On the toast's own fill the mark takes the toast's ink, which is made
   to read on it in both themes. */
.ssb-toast .ssb-success { color: inherit; }
/* How long it has left: a line along its foot that shortens to nothing.
   toast() drives it, from the right edge, by transform only. */
.ssb-toast__time {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: currentColor;
  opacity: 0.45;
  transform-origin: left;
}

/* ---------- Success mark ------------------------------------
   A ring and a tick that arrive with a small pop and a halo that spreads
   once and fades. Played by @starting-style, so only as it appears. Under
   reduced motion it fades in where it is. */
.ssb-success {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  color: var(--ok-text);
  transition: transform var(--dur-sheet) var(--ease-out), opacity var(--dur-base) var(--ease);
}
.ssb-success__halo {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: currentColor;
  opacity: 0;
  transform: scale(1.8);
  transition: transform var(--dur-sheet) var(--ease-out), opacity var(--dur-sheet) var(--ease-out);
}
@starting-style {
  .ssb-success { opacity: 0; transform: scale(0.5); }
  .ssb-success__halo { opacity: 0.35; transform: scale(0.6); }
}
@media (prefers-reduced-motion: reduce) {
  .ssb-success, .ssb-success__halo { transition-property: opacity; }
}

/* ---------- Copy button -------------------------------------
   Its icon turns into a tick for two seconds; the new icon pops in. */
.ssb-copy[data-done] { color: var(--ok-text); border-color: var(--ok-line); }
.ssb-copy__mark { display: inline-flex; }
.ssb-copy[data-done] .ssb-copy__mark .ssb-icon { transition: transform var(--dur-base) var(--ease-out); }
@starting-style {
  .ssb-copy[data-done] .ssb-copy__mark .ssb-icon { transform: scale(0.4); }
}

@media (prefers-reduced-motion: reduce) {
  .ssb-toast { --toast-from: none; }
}
@starting-style {
  .ssb-toast { opacity: 0; transform: var(--toast-from); }
}
/* Set by toast() while it leaves. */
.ssb-toast[data-closing] {
  opacity: 0;
  transform: var(--toast-from);
  transition-duration: var(--dur-exit);
}

/* ---------- Marking a date ----------------------------------
   Today and the day being viewed, in every place a date is drawn. The
   values are tokens (tokens.css, "Marking a date"); this is the one rule
   that applies them, so the date block, the month and the wall week
   cannot disagree about what today looks like. */

.ssb-date.ssb-date--today,
.ssb-month__day--today .ssb-month__num,
.ssb-week__head--today .ssb-week__date {
  background: var(--today-bg);
  color: var(--today-ink);
  box-shadow: var(--today-shadow);
  border-radius: var(--radius-mark);
}
.ssb-date.ssb-date--selected,
.ssb-month__day--selected .ssb-month__num,
.ssb-week__head--selected .ssb-week__date {
  background: var(--selected-bg);
  color: var(--selected-ink);
  box-shadow: var(--selected-shadow);
  border-radius: var(--radius-mark);
}

/* ---------- Icons -------------------------------------------
   Drawn by icon() in ui.js, from a closed list. Stroked in the colour of
   the text beside them, so they flip with the theme for free. */

.ssb-icon { width: var(--icon-lg); height: var(--icon-lg); flex-shrink: 0; }
.ssb-icon--md { width: var(--icon-md); height: var(--icon-md); }

/* ---------- The date block ----------------------------------
   A small weekday over a large numeral. A day that is over is lighter in
   weight, not faded, so it stays readable. */

.ssb-date {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-width: 2.2em;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-mark);
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-tight);
  color: var(--text-strong);
}
.ssb-date__dow { font-size: var(--text-xs); font-weight: var(--fw-medium); color: inherit; }
.ssb-date__num { font-weight: var(--fw-heavy); letter-spacing: var(--tracking-tight); }
.ssb-date--sm .ssb-date__num { font-size: var(--text-lg); }
.ssb-date--md .ssb-date__num { font-size: var(--text-2xl); }
.ssb-date--past { color: var(--text-muted); }
/* A day that is coming, not started: a dashed edge, drawn inside the block
   so it is the same size as an unmarked one ("dashed means asked, not real
   yet", STYLE.md). Never on today or the day on show. */
.ssb-date--coming { outline: 1px dashed var(--line-strong); outline-offset: -1px; }
.ssb-date--past .ssb-date__num { font-weight: var(--fw-regular); }

/* ---------- Period navigation -------------------------------
   Previous, the period in large type, next. The arrows are the size of the
   touch target and the chevron fills most of it — a 14px character in the
   middle of a 44px box is a speck. The period is two lines, a kicker over
   the date, because the date is what somebody came to read. "Today" is a
   separate button (todayButton), so a long title never has to share its
   row with it. */

.ssb-nav { display: flex; align-items: center; gap: var(--space-2); }
.ssb-nav__btn {
  display: inline-grid;
  place-items: center;
  flex-shrink: 0;
  width: var(--touch-min);
  height: var(--touch-min);
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-plate);
  background: var(--surface);
  color: var(--text-strong);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-press) var(--ease-out);
}
.ssb-nav__btn:active:not(:disabled) { transform: scale(var(--press-scale)); }
.ssb-nav__btn:disabled { opacity: 0.45; cursor: not-allowed; }
@media (hover: hover) and (pointer: fine) {
  .ssb-nav__btn:hover:not(:disabled) { border-color: var(--text-strong); background: var(--surface-hover); }
}
.ssb-nav__title {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding-inline: var(--space-1);
  line-height: var(--lh-tight);
}
.ssb-nav__kicker { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--text-muted); }
.ssb-nav__main {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--fw-heavy);
  letter-spacing: var(--tracking-tight);
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}
/* The narrowest phones: a month-crossing week no longer fits at full size. */
@media (max-width: 22.5rem) {
  .ssb-nav__main { font-size: var(--text-xl); }
}

/* The word on a tile — an apartment label, Free, Yours — is read before
   anything else on it, so it is set in the display face. */
.ssb-cell__state { font-family: var(--font-display); font-weight: var(--fw-heavy); }

/* ============================================================
   Lists and forms: what a row, a state, a notice and a field are.

   Everything above was written for the calendar. These are for the
   person who runs the building, at a desk, doing one errand. The
   first screens built for them were a bold line, a grey line and an
   outlined button, repeated, because this vocabulary did not exist.
   design/review-2026-10.md is the review that found it; STYLE.md,
   "Lists and forms", says when each piece is used.
   ============================================================ */

.ssb-icon--sm { width: var(--icon-sm); height: var(--icon-sm); }

/* ---------- A state that is not a slot's --------------------
   Its icon, its word, its ink. Still not a chip. The four slot states
   above stay bare words; these always carry their icon, so "done" and
   "needs somebody" are never told apart by green and amber alone. */
.ssb-state--ok,
.ssb-state--attention,
.ssb-state--blocked,
.ssb-state--info {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}
.ssb-state--ok        { color: var(--ok-text); }
.ssb-state--attention { color: var(--attention-text); }
.ssb-state--blocked   { color: var(--danger-text); }
.ssb-state--info      { color: var(--text-body); }

/* ---------- A notice with something to say -------------------
   The icon and the thing named are in the tone's ink; the sentence is
   in body colour, because a paragraph of amber on amber is the first
   thing to fail in the other theme. */
.ssb-notice--icon {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-3);
  color: var(--text-body);
}
.ssb-notice--icon > .ssb-icon { color: var(--text-body); }
.ssb-notice--icon.ssb-notice--error   > .ssb-icon, .ssb-notice--error   .ssb-notice__title { color: var(--danger-text); }
.ssb-notice--icon.ssb-notice--success > .ssb-icon, .ssb-notice--success .ssb-notice__title { color: var(--ok-text); }
.ssb-notice--icon.ssb-notice--warn    > .ssb-icon, .ssb-notice--warn    .ssb-notice__title { color: var(--attention-text); }
.ssb-notice__body {
  /* Wide enough to read; a button that does not fit beside it goes under. */
  flex: 1 1 14rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* Lines the text up with the icon's middle, whatever the icon's size. */
  padding-top: 1px;
}
.ssb-notice__title { font-weight: var(--fw-bold); color: var(--text-strong); }
.ssb-notice--icon > .ssb-btn { flex: 0 1 auto; align-self: center; margin-left: auto; }

/* ---------- Rows ---------------------------------------------
   Ruled, not boxed. A mark leads each row so two rows can be told
   apart before either is read. The row is the target; a row that is
   fine has a chevron and nothing else to press. */
.ssb-rows {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.ssb-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  border-top: 1px solid var(--line);
}
.ssb-row:first-child { border-top: 0; }

.ssb-row__main {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: calc(var(--touch-min) + var(--space-5));
  padding: var(--space-3);
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
}
button.ssb-row__main { cursor: pointer; transition: background-color var(--dur-fast) var(--ease); }
/* A full-width row darkens under a press. At this width a scale reads as
   the list jumping. */
button.ssb-row__main:active { background: var(--surface-press); }
@media (hover: hover) and (pointer: fine) {
  button.ssb-row__main:hover { background: var(--surface-hover); }
}
/* The ring would be cut off by the list's rounded corner. */
button.ssb-row__main { --ring-gap: calc(var(--ring-width) * -1); }
.ssb-row__main > .ssb-icon { color: var(--text-muted); }

/* Every mark sits in the same column, whatever it is: a tile, a plate, a
   date. Otherwise the titles down a mixed list start at three different
   places. */
.ssb-row__mark { flex-shrink: 0; display: inline-flex; justify-content: center; min-width: var(--touch-min); }
.ssb-row__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ssb-row__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; column-gap: var(--space-3); row-gap: 2px; }
.ssb-row__title {
  font-size: var(--text-md);
  font-weight: var(--fw-bold);
  color: var(--text-strong);
  overflow-wrap: anywhere;
}
/* The second line is data as often as it is commentary (a name, an address,
   a schedule), so it is body colour. Muted is for what can be skipped. */
.ssb-row__sub {
  font-size: var(--text-sm);
  line-height: var(--lh-snug);
  color: var(--text-body);
  overflow-wrap: anywhere;
}
.ssb-row__action { flex-shrink: 0; padding-right: var(--space-3); }
/* The household a booking is: its plate, and anything said about it. What
   is said is a sentence, and wraps rather than running off a narrow row. */
.ssb-row__who { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; max-width: 100%; }
.ssb-row__who > .ssb-state { flex-shrink: 1; min-width: 0; white-space: normal; align-items: flex-start; }
.ssb-row__who > .ssb-state > .ssb-icon { flex-shrink: 0; margin-top: 2px; }

/* A row that needs somebody: an edge on the leading side, drawn as an inset
   shadow so the row is not a pixel wider than its neighbours. Its state
   word, with the alert, says what. */
.ssb-row--attention { box-shadow: inset 3px 0 0 var(--attention-line); }

/* On a phone the one next step goes under the text, at full width, rather
   than squeezing the title into a column of two words. */
@media (max-width: 640px) {
  .ssb-row--action { flex-direction: column; align-items: stretch; gap: 0; }
  .ssb-row__action { padding: 0 var(--space-3) var(--space-3); }
  .ssb-row__action > .ssb-btn { width: 100%; }
}

/* ---------- A kind of thing, as a mark ---------------------- */
.ssb-tile {
  display: inline-flex;
  /* A mark keeps its size beside a title of any length. */
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  border-radius: var(--radius);
  background: var(--surface-sunk);
  color: var(--text-strong);
}

/* ---------- Fields -------------------------------------------
   A label, the control, and one line under it. The line is always there,
   so a message arriving does not push the form down the page. */
.ssb-field { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.ssb-field__label {
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--text-body);
}
.ssb-field__optional { margin-left: var(--space-2); font-weight: var(--fw-regular); color: var(--text-muted); }

.ssb-field__box { position: relative; display: flex; align-items: center; gap: var(--space-2); }
.ssb-field__control {
  flex: 1;
  min-width: 0;
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-strong);
  font: inherit;
  font-size: var(--text-md);
  transition: border-color var(--dur-fast) var(--ease);
}
.ssb-field__control:focus-visible { border-color: var(--accent); }
/* Room for the status icon, so a value never runs under it. */
.ssb-field__box--status .ssb-field__control { padding-right: calc(var(--space-3) * 2 + var(--icon-md)); }
.ssb-field__status {
  position: absolute;
  right: var(--space-3);
  top: 50%;
  display: inline-flex;
  transform: translateY(-50%);
  pointer-events: none;
}
/* With a unit beside the control the box is wider than the control, so the
   icon is placed in the flow, pulled back over the control's own edge. */
.ssb-field__box--unit .ssb-field__status {
  position: static;
  transform: none;
  flex-shrink: 0;
  width: var(--icon-md);
  margin-left: calc((var(--icon-md) + var(--space-3) + var(--space-2)) * -1);
  margin-right: var(--space-3);
}
.ssb-field__unit { flex-shrink: 0; font-size: var(--text-sm); color: var(--text-body); }

.ssb-field__line {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: 0;
  min-height: calc(var(--text-sm) * var(--lh-snug));
  font-size: var(--text-sm);
  line-height: var(--lh-snug);
  color: var(--text-muted);
}
.ssb-field__line > .ssb-icon { margin-top: 1px; }

/* The hint, and beside it how much room is left. Never red: running out is
   not a mistake, and typing simply stops. */
.ssb-field__foot { display: flex; align-items: flex-start; gap: var(--space-3); }
.ssb-field__foot > .ssb-field__line { flex: 1; min-width: 0; }
.ssb-field__count {
  flex-shrink: 0;
  font-size: var(--text-sm);
  line-height: var(--lh-snug);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.ssb-field__count--low { color: var(--text-body); font-weight: var(--fw-bold); }

/* ---------- Known problems — what residents reported ---------
   Under the space buttons, on the phone and the wall screen. Ruled, with
   the attention edge rows use, a mark and a word: three carriers, never a
   tinted box. When nothing is reported only the report button is left, at
   the same place, so it never moves under a finger. */
.ssb-problems {
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  box-shadow: inset 3px 0 0 var(--attention-line);
}
.ssb-problems--none { background: transparent; border-color: transparent; box-shadow: none; padding-block: 0; }
/* A long count and the button share a line until they cannot; then the
   button goes under, never off the edge. */
.ssb-problems__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); }
.ssb-problems--none .ssb-problems__head { justify-content: flex-end; }
.ssb-problems__count { display: inline-flex; align-items: center; gap: var(--space-2); white-space: nowrap; }
.ssb-problems__count > .ssb-icon { color: var(--attention-text); }
.ssb-problems__report { flex-shrink: 0; }
.ssb-problems__list { margin: 0; padding: 0; list-style: none; }
.ssb-problems__item {
  display: grid;
  grid-template-columns: 4.5em minmax(0, 1fr) auto;
  gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-2) 0;
  border-top: 1px solid var(--line);
  font-size: var(--text-sm);
  line-height: var(--lh-normal);
}
.ssb-problems__date { font-weight: var(--fw-bold); }
.ssb-problems__text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.ssb-problems__more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--touch-min);
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--fw-bold);
}
button.ssb-problems__more { cursor: pointer; }
.ssb-problems__footnote { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--text-muted); }

.ssb-field--valid .ssb-field__status { color: var(--ok-text); }

/* Wrong: the edge, an icon in the control, an icon and the fix under it.
   The inset doubles the edge without making the box bigger. */
.ssb-field--invalid .ssb-field__control { border-color: var(--danger-edge); box-shadow: inset 0 0 0 1px var(--danger-edge); }
.ssb-field--invalid .ssb-field__status,
.ssb-field--invalid .ssb-field__line { color: var(--danger-text); }

/* ---------- A day box ---------------------------------------
   dayField() in ui.js: the face is drawn (a coming date block, the day in
   words, the calendar at the end) and the real date input lies over it,
   invisible, so a tap opens the device's own picker. One height in every
   state, sunk or set or empty, so choosing moves nothing under it. */

.ssb-dayfield {
  height: 4rem;
  padding: 0 var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-strong);
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.ssb-dayfield--md { height: 5.5rem; }
.ssb-dayfield__face { display: flex; align-items: center; gap: var(--space-3); flex: 1; min-width: 0; pointer-events: none; }
.ssb-dayfield__face > .ssb-icon:last-child { margin-left: auto; }
.ssb-dayfield__words { display: grid; min-width: 0; }
.ssb-dayfield__day { font-weight: var(--fw-bold); white-space: nowrap; }
.ssb-dayfield--md .ssb-dayfield__day { font-size: var(--text-lg); white-space: normal; }
.ssb-dayfield__far { font-size: var(--text-sm); color: var(--text-body); }
.ssb-dayfield .ssb-dayfield__input {
  position: absolute;
  inset: 0;
  min-height: 0;
  opacity: 0;
  cursor: pointer;
}
.ssb-dayfield:focus-within { border-color: var(--accent); outline: var(--ring-width) solid var(--ring-color); outline-offset: var(--ring-gap); }
.ssb-dayfield:active:not(.ssb-dayfield--sunk) { background: var(--surface-press); }
@media (hover: hover) and (pointer: fine) {
  .ssb-dayfield:hover:not(.ssb-dayfield--sunk) { border-color: var(--text-strong); }
}
.ssb-dayfield--sunk { background: var(--surface-sunk); border-color: var(--line); }
.ssb-dayfield--sunk .ssb-dayfield__day { font-weight: var(--fw-medium); color: var(--text-body); }
.ssb-dayfield--sunk .ssb-dayfield__input { cursor: default; }
.ssb-field--invalid .ssb-dayfield { border-color: var(--danger-line); box-shadow: inset 0 0 0 1px var(--danger-line); }
.ssb-field--invalid .ssb-dayfield__face > .ssb-icon:nth-last-child(2) { margin-left: auto; color: var(--danger-text); }
.ssb-field--invalid .ssb-dayfield__face > .ssb-icon:last-child { margin-left: 0; }

/* Cannot be changed here: a sunk ground and a faint edge say so. What it
   holds is still read (a timezone, an address), so it keeps body colour. */
.ssb-field--disabled .ssb-field__control { border-color: var(--line); background: var(--surface-sunk); color: var(--text-body); cursor: not-allowed; }
.ssb-field--disabled .ssb-field__line,
.ssb-field--readonly .ssb-field__line { color: var(--text-body); }
/* Shown, not offered: no edge, the same size, so a column of fields with one
   fixed value in it still lines up. */
.ssb-field--readonly .ssb-field__control { border-color: transparent; background: transparent; padding-left: 0; }

/* ---------- A drop-down that finds (combobox) ----------------
   The list drops from the box and is joined to it: no gap, the box's
   bottom corners squared, one accent edge round both, so the two read as
   one thing (the owner: "small gap in the line for the dropdown"). It sits
   in the flow, never floating: a sheet's scrolling cannot clip it. Five
   and a half rows tall, so the half row says there is more; it scrolls
   inside itself and never hands the scroll on to the sheet. Each row is a
   touch target; pressed darkens, the highlighted one has a tint and an
   inset edge, the one already picked carries a tick. */
.ssb-field__box--pick .ssb-field__control { padding-right: calc(var(--touch-min) + var(--space-1)); }
.ssb-field__box--pick.ssb-field__box--status .ssb-field__control { padding-right: calc(var(--touch-min) + var(--icon-md) + var(--space-3)); }
.ssb-field__box--pick .ssb-field__status { right: var(--touch-min); }
.ssb-pick__toggle {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-body);
  cursor: pointer;
}
/* Points up while the list is down. Instant: it says where the list is. */
.ssb-field__box--open .ssb-pick__toggle > .ssb-icon { transform: rotate(180deg); }
.ssb-field__box--open .ssb-field__control {
  border-color: var(--accent);
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.ssb-pick {
  list-style: none;
  margin: 0;
  padding: var(--space-1);
  max-height: calc(var(--touch-min) * 5.5 + var(--space-1));
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--accent);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--surface);
}
.ssb-pick[hidden] { display: none; }
/* Joined, with the ring as well: the ring is drawn on the box's own edge
   and the list takes the same width and colour of line, so the two are one
   outline with the box's lower edge as a divider. */
.ssb-field__box--open .ssb-field__control:focus-visible { outline-offset: -1px; }
.ssb-field__box--open:has(.ssb-field__control:focus-visible) + .ssb-pick {
  border-width: 0 var(--ring-width) var(--ring-width);
  border-color: var(--ring-color);
}
/* A drawer that holds a drop-down is opened tall enough for its list, so
   dropping it down pushes the fields below and never moves the box that was
   just tapped, or the sheet's top, under the finger. */
@media (max-width: 639.98px) {
  .ssb-sheet:has(.ssb-field__box--pick) { min-height: min(90dvh, 46rem); }
}
/* Joined: the field's own gap between box and hint is not between box and list. */
.ssb-field__box--open { margin-bottom: calc(var(--space-1) * -1); }
.ssb-pick__option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-min);
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--text-strong);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}
.ssb-pick__option--active { background: var(--accent-tint); box-shadow: inset 0 0 0 1px var(--accent); }
.ssb-pick__option:active { background: var(--surface-press); }
@media (hover: hover) and (pointer: fine) {
  .ssb-pick__option:hover:not(.ssb-pick__option--active) { background: var(--surface-sunk); }
}
.ssb-pick__option .ssb-apt { flex-shrink: 0; min-width: 3rem; text-align: center; font-size: var(--text-xs); }
.ssb-pick__option > .ssb-icon { flex: none; margin-left: auto; color: var(--accent); }
.ssb-pick__line {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
}
.ssb-pick__line--muted { color: var(--text-muted); }
.ssb-pick__more,
.ssb-pick__none {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-body);
}

/* ---------- Steps --------------------------------------------
   One line that says where you are and takes you back. A tick for done,
   a number otherwise, a ring on the step on show. */
.ssb-steps { display: flex; flex-direction: column; gap: var(--space-2); }
.ssb-steps__list {
  display: flex;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ssb-steps__step { display: flex; align-items: center; min-width: 0; }
/* The line between two steps, and it is only a line: it does not turn green
   behind you, which would be a second place for the state to disagree. */
.ssb-steps__step + .ssb-steps__step { flex: 1; }
.ssb-steps__step + .ssb-steps__step::before {
  content: "";
  flex: 1;
  min-width: var(--space-3);
  height: 1px;
  background: var(--line-strong);
}
.ssb-steps__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-body);
  font: inherit;
  font-size: var(--text-sm);
  white-space: nowrap;
}
button.ssb-steps__btn { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  button.ssb-steps__btn:hover .ssb-steps__mark { background: var(--surface-press); }
}
.ssb-steps__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-mark);
  background: var(--surface-sunk);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
}
.ssb-steps__step--done .ssb-steps__mark { background: var(--ok-fill); color: var(--ok-text); }
/* A step that has just been done: its tick pops in, once. */
.ssb-steps__mark--fresh .ssb-icon { transition: transform var(--dur-sheet) var(--ease-out); }
@starting-style {
  .ssb-steps__mark--fresh .ssb-icon { transform: scale(0.3); }
}
@media (prefers-reduced-motion: reduce) {
  .ssb-steps__mark--fresh .ssb-icon { transition: none; }
}

/* ---------- Progress -----------------------------------------
   "2 of 3 done" over a bar. The bar is a fill scaled from the left, so it
   grows by transform when a step is done and is otherwise still. Done is
   the word as well as the full bar. */
.ssb-progress { display: flex; flex-direction: column; gap: var(--space-1); }
.ssb-progress__words { font-size: var(--text-xs); color: var(--text-body); font-variant-numeric: tabular-nums; }
.ssb-progress__track {
  display: block;
  height: 6px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  /* A line, not the sunk surface: the sunk grey all but vanishes on the
     dark page, and an empty bar must still be seen to be empty. */
  background: var(--line);
}
.ssb-progress__fill {
  --progress: 0;
  display: block;
  height: 100%;
  background: var(--ok-line);
  transform: scaleX(var(--progress));
  transform-origin: left;
}
/* The growth is played by ui.js (slideFrom), over the bar already drawn at
   its new length, and not at all under reduced motion. */
.ssb-steps__step--on .ssb-steps__mark {
  background: var(--accent-tint);
  box-shadow: inset 0 0 0 2px var(--accent);
  color: var(--text-strong);
  font-weight: var(--fw-bold);
}
.ssb-steps__step--on .ssb-steps__name { color: var(--text-strong); font-weight: var(--fw-bold); }
.ssb-steps__now { margin: 0; font-size: var(--text-sm); color: var(--text-body); }
.ssb-steps__now strong { color: var(--text-strong); }

/* At a desk every step is named beside its mark, so the line under is not
   needed. On a phone five names do not fit: marks only, and the line under
   names the one on show. The name stays in the button's label either way. */
@media (min-width: 641px) {
  .ssb-steps__now { display: none; }
  .ssb-steps__btn { padding: 0 var(--space-2) 0 0; }
}
@media (max-width: 640px) {
  .ssb-steps__name { display: none; }
}

/* ---------- Stepper ----------------------------------------- */
.ssb-stepper { display: inline-flex; align-items: center; gap: var(--space-1); }
.ssb-stepper__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-strong);
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out), background-color var(--dur-fast) var(--ease);
}
.ssb-stepper__btn:active:not(:disabled) { transform: scale(var(--press-scale)); }
.ssb-stepper__btn:disabled { border-color: var(--line); color: var(--text-faint); cursor: not-allowed; }
@media (hover: hover) and (pointer: fine) {
  .ssb-stepper__btn:hover:not(:disabled) { background: var(--surface-hover); }
}
.ssb-stepper__value {
  width: 3rem;
  min-height: var(--touch-min);
  border: 0;
  background: transparent;
  color: var(--text-strong);
  font: inherit;
  font-size: var(--text-lg);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* ---------- Days of the week ---------------------------------
   Chosen and not chosen differ in four ways, so neither can be mistaken
   for the other at a glance: a chosen day is solid accent, its text in
   --on-accent, bold, with a tick above its name; a day that is not chosen
   is plain white, a faint edge, regular weight, no tick. The tick grows in
   when a finger chooses. Solid on Ivan's word (2026-10-20), over a tint and
   then a gradient: "Solid is the best". */
.ssb-daypick { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.ssb-daypick__day {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  flex: 1 0 var(--touch-min);
  min-height: calc(var(--touch-min) + var(--space-2));
  padding: 0 var(--space-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-body);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.ssb-daypick__day:active { transform: scale(var(--press-scale)); }
.ssb-daypick__tick {
  display: inline-flex;
  opacity: 0;
  transform: scale(0.5);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.ssb-daypick__day[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: var(--fw-bold);
}
.ssb-daypick__day[aria-pressed="true"] .ssb-daypick__tick { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  /* The tick still fades; it no longer grows. */
  .ssb-daypick__tick { transform: none; }
}
@media (hover: hover) and (pointer: fine) {
  .ssb-daypick__day:hover { border-color: var(--ink-400); }
  .ssb-daypick__day[aria-pressed="true"]:hover { border-color: var(--accent-hover); }
}

/* ---------- A length of time, and a count ---------------------
   A number and its unit on one row, close: they are one answer. Under
   them, what it is counted from and the range in that unit. Off, it stays
   where it is, greyed, so the setting that holds it does not change height.
   Refused, it is a field's invalid state: the edge doubled by an inset, the
   alert inside the box (which keeps room for it at rest, so nothing beside
   it moves), and the line under it taken over by the alert and the fix. A
   count is the same box, with its word after it instead of a unit choice. */
.ssb-length { display: flex; flex-direction: column; gap: var(--space-1); }
.ssb-length__row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.ssb-length__box { position: relative; display: inline-flex; flex-shrink: 0; }
.ssb-length__number {
  width: 5rem;
  min-height: var(--touch-min);
  padding: 0 calc(var(--space-2) * 2 + var(--icon-md)) 0 var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-strong);
  font: inherit;
  font-size: var(--text-md);
  transition: border-color var(--dur-fast) var(--ease);
  /* The spinner sat where the alert goes; arrow keys still step the number. */
  appearance: textfield;
  -moz-appearance: textfield;
}
.ssb-length__number::-webkit-inner-spin-button,
.ssb-length__number::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.ssb-length__number:focus-visible { border-color: var(--accent); }
.ssb-length__status {
  position: absolute;
  right: var(--space-2);
  top: 50%;
  display: inline-flex;
  transform: translateY(-50%);
  color: var(--danger-text);
  pointer-events: none;
}
.ssb-length__line {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: 0;
  min-height: calc(var(--text-sm) * var(--lh-snug));
  font-size: var(--text-sm);
  line-height: var(--lh-snug);
  color: var(--text-body);
}
.ssb-length__line > .ssb-icon { margin-top: 1px; }
.ssb-length--invalid .ssb-length__number { border-color: var(--danger-edge); box-shadow: inset 0 0 0 1px var(--danger-edge); }
.ssb-length--invalid .ssb-length__line { color: var(--danger-text); }
.ssb-count__suffix { font-size: var(--text-sm); color: var(--text-body); }
.ssb-length[data-off] { opacity: 0.45; }

/* ---------- A span of time -----------------------------------
   Times or All day, then From and To each on a row, close together: the
   big gap is around the pair, not inside it. "next day" is a word in
   weight beside To, there only when To is earlier than From. */
.ssb-span { display: flex; flex-direction: column; gap: var(--space-3); }
.ssb-span > .ssb-segmented { align-self: flex-start; }
.ssb-span__times { display: flex; flex-direction: column; gap: var(--space-2); }
/* Wide enough for the time, its clock, and the alert after them. */
.ssb-span__times .ssb-field__control { max-width: 10.5rem; }
/* A field that never speaks takes no room for a line it will not use. To
   does speak: its hint, then nothing once "next day" says it, then perhaps a
   refusal. Its line keeps its height through all three, and that height is
   two lines: an overlap's refusal names the other window and its times, and
   takes two at phone width. */
.ssb-span__times .ssb-field__line:empty { display: none; }
.ssb-span__times .ssb-span__end .ssb-field__line {
  display: flex;
  min-height: calc(2 * var(--text-sm) * var(--lh-snug));
}
.ssb-span__next { font-weight: var(--fw-bold); color: var(--text-strong); }
.ssb-span__next[hidden] { display: none; }

/* ---------- A setting ----------------------------------------
   Its name and switch share the first line and the switch is in the same
   place on or off. The sentence keeps room for two lines and what the
   setting holds stays on screen while it is off: switching moves nothing. */
.ssb-setting {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--line);
}
.ssb-setting:first-of-type { border-top: 0; }
.ssb-setting > * { grid-column: 1 / -1; }
.ssb-setting > .ssb-setting__label { grid-column: 1; grid-row: 1; align-self: center; }
.ssb-setting > .ssb-setting__switch { grid-column: 2; grid-row: 1; }
.ssb-setting__label { margin: 0; font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--text-strong); }
.ssb-setting__sentence { margin: 0; font-size: var(--text-sm); line-height: var(--lh-snug); color: var(--text-body); }
.ssb-setting__sentence--changes { min-height: calc(2em * var(--lh-snug)); }

/* ---------- Save bar -----------------------------------------
   At the foot of the screen while its form is in view, and only when
   something has changed. A line round it, no shadow: a boundary is a line.
   While saving, the button keeps its fill and says so with a spinner. */
.ssb-savebar {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
  padding: var(--space-3) var(--space-4);
  padding-bottom: max(var(--space-3), env(safe-area-inset-bottom));
  border: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--surface);
}
.ssb-savebar[hidden] { display: none; }
/* One run of text: the alert, the sentence and Show wrap as words do, so a
   long sentence never leaves the alert alone on a line above it. */
.ssb-savebar__words {
  flex: 1 1 12rem;
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--lh-snug);
  color: var(--text-strong);
}
.ssb-savebar__words > .ssb-icon { vertical-align: -3px; margin-right: var(--space-1); }
.ssb-savebar__buttons { display: flex; gap: var(--space-2); margin-left: auto; }
.ssb-savebar__save { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-width: 7rem; }
.ssb-savebar__show {
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-strong);
  font: inherit;
  font-weight: var(--fw-bold);
  text-decoration: underline;
  cursor: pointer;
}

/* ---------- Board notices ------------------------------------
   design/briefs/board-notices.md. A band at the top of the calendar and
   the wall screen: one ruled line a notice, the headline in strong ink,
   when it was posted after it. Urgent is the attention fill, a leading
   inset edge, the alert icon and the word — never the fill alone. A tap
   opens the details, at once: it is read several times a visit. Ruled,
   not boxed, and square on the wall like the rest of the kiosk. */
.ssb-board {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin: var(--space-2) 0 var(--space-3);
}
.ssb-board__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  column-gap: var(--space-3);
  align-items: center;
  padding: var(--space-1) var(--space-3);
  background: var(--surface);
  color: var(--text-body);
}
.ssb-board__item + .ssb-board__item,
.ssb-board__list + .ssb-board__line,
.ssb-board__line + .ssb-board__line { border-top: 1px solid var(--line); }
.ssb-board__item > .ssb-icon { color: var(--text-muted); }
.ssb-board__item--urgent {
  background: var(--attention-fill);
  box-shadow: inset 4px 0 0 var(--attention-line);
}
.ssb-board__item--urgent > .ssb-icon { color: var(--attention-text); }
.ssb-board__toggle {
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  padding: var(--space-2) 0;
  min-height: var(--touch-min);
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
}
button.ssb-board__toggle { cursor: pointer; }
.ssb-board__urgent { font-weight: var(--fw-bold); color: var(--attention-text); margin-right: var(--space-2); }
.ssb-board__head {
  font-weight: var(--fw-bold);
  color: var(--text-strong);
  line-height: var(--lh-snug);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.ssb-board__meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  display: flex;
  gap: var(--space-1);
  align-items: center;
}
/* Turned at once, like the details it stands for. */
.ssb-board__item--open .ssb-board__meta > .ssb-icon { transform: rotate(180deg); }
.ssb-board__details {
  grid-column: 2 / -1;
  margin: 0;
  padding: 0 0 var(--space-3);
  white-space: pre-line;
  overflow-wrap: anywhere;
  line-height: var(--lh-normal);
}
.ssb-board__hide {
  appearance: none;
  border: 0;
  background: none;
  color: var(--text-muted);
  width: var(--touch-min);
  height: var(--touch-min);
  margin-right: calc(-1 * var(--space-2));
  display: grid;
  place-items: center;
  cursor: pointer;
}
.ssb-board__line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  padding: 0 var(--space-3);
  min-height: var(--touch-min);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.ssb-board__more {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  min-height: var(--touch-min);
  color: var(--text-strong);
  font-weight: var(--fw-medium);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
/* An event (notice-events.md): marked by its date, drawn the one way a date
   is, and its day and time on a line of their own under the headline, with
   the way into a calendar on that line. The time is a date: the display face,
   tabular, never muted. "Posted" is not shown for it (R20). */
.ssb-board__item--event > .ssb-date { align-self: start; margin-top: var(--space-2); }
.ssb-board__event {
  grid-column: 2 / -1;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  column-gap: var(--space-3);
  min-height: var(--touch-min);
  margin-top: calc(-1 * var(--space-3));
}
.ssb-board__when {
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-medium);
  color: var(--text-body);
  white-space: nowrap;
}
.ssb-board__add { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-sm); white-space: nowrap; }
/* The details' chevron, at the end of an event's time line on a phone. */
.ssb-board__chev {
  appearance: none;
  border: 0;
  background: none;
  color: var(--text-muted);
  width: var(--touch-min);
  height: var(--touch-min);
  margin: 0 calc(-1 * var(--space-2)) 0 auto;
  display: grid;
  place-items: center;
  cursor: pointer;
}
.ssb-board__item--open .ssb-board__chev > .ssb-icon { transform: rotate(180deg); }
.ssb-board__chev:active { color: var(--text-strong); }
@media (hover: hover) and (pointer: fine) {
  .ssb-board__chev:hover { color: var(--text-strong); }
}
.ssb-board--wall .ssb-board__when { font-size: var(--text-lg); }
.ssb-board--wall .ssb-board__item--event > .ssb-date { align-self: center; margin-top: 0; }

/* "Add to your calendar": the event as the band showed it, then the two ways
   in, as rows (app.js openAddToCalendar). */
.ssb-add-calendar__event {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  margin: var(--space-2) 0 var(--space-4);
}
.ssb-add-calendar__event p { margin: 0; }
.ssb-add-calendar__headline { font-weight: var(--fw-bold); color: var(--text-strong); overflow-wrap: anywhere; }
.ssb-add-calendar__when { font-variant-numeric: tabular-nums; color: var(--text-body); }

/* A press is answered on press. A full-width line darkens rather than
   scaling; the hide button does the same in its own square. */
.ssb-board__item:not(.ssb-board__item--urgent):has(> button.ssb-board__toggle:active) { background: var(--surface-press); }
.ssb-board__hide:active, .ssb-board__more:active { color: var(--text-strong); }
@media (hover: hover) and (pointer: fine) {
  .ssb-board__item:not(.ssb-board__item--urgent):has(> button.ssb-board__toggle:hover) { background: var(--surface-hover); }
  .ssb-board__hide:hover { color: var(--text-strong); }
}

/* The wall: one line a notice, read from three metres, a touch target a
   hand finds without looking. The kiosk's scale makes the type bigger. */
.ssb-board--wall { margin: 0; }
.ssb-board--wall .ssb-board__item { padding: 0 var(--space-4); }
.ssb-board--wall .ssb-board__item--urgent { box-shadow: inset 6px 0 0 var(--attention-line); }
.ssb-board--wall .ssb-board__toggle {
  flex-direction: row;
  align-items: center;
  gap: var(--space-4);
  min-height: var(--touch-kiosk);
  padding: 0;
}
.ssb-board--wall .ssb-board__head { font-size: var(--text-lg); -webkit-line-clamp: 1; flex: 1 1 auto; }
.ssb-board--wall .ssb-board__meta { font-size: var(--text-md); flex: 0 0 auto; }
.ssb-board--wall .ssb-board__details { font-size: var(--text-md); }
.ssb-board--wall .ssb-board__line { font-size: var(--text-md); color: var(--text-body); padding: 0 var(--space-4); }

/* ---------- A household ----------------------------------------
   design/briefs/households.md. A person is a round initial, tinted by
   their place in the household; a fob is a small plate that hangs under
   its person like a key on a ring; a child's fob log is a timeline, the
   one place on these screens with any drama. Never on the wall screen. */

.ssb-person {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  inline-size: 2rem;
  block-size: 2rem;
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--fw-heavy);
  line-height: var(--lh-tight);
  background: var(--person-5-fill);
  color: var(--person-5-text);
}
.ssb-person--sm { inline-size: 1.5rem; block-size: 1.5rem; font-size: var(--text-xs); }
.ssb-person--1 { background: var(--person-1-fill); color: var(--person-1-text); }
.ssb-person--2 { background: var(--person-2-fill); color: var(--person-2-text); }
.ssb-person--3 { background: var(--person-3-fill); color: var(--person-3-text); }
.ssb-person--4 { background: var(--person-4-fill); color: var(--person-4-text); }
/* A child: a ring in the person's own ink, set off from the disc. An
   outline, so it moves nothing; never a control's focus look, which is the
   accent's ring on something you can press. The word "child" is always
   beside it, so the ring is never the only sign. */
.ssb-person--child { outline: 2px solid currentColor; outline-offset: 2px; }

/* A fob: a flat plate, like the apartment's, because nothing is done to it by
   tapping it (an outline means you can act on it). Switched off: no fill,
   struck, in muted ink, and the word beside it wherever it is shown. */
.ssb-fobs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.ssb-fob {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-block-size: 1.75rem;
  padding: 0 var(--space-2);
  border-radius: var(--radius-plate);
  background: var(--taken-fill);
  color: var(--text-strong);
  font-size: var(--text-sm);
}
.ssb-fob--off { background: transparent; color: var(--text-muted); text-decoration: line-through; }

/* My household: one line a person, the fobs and a child's log under it. */
.ssb-household__person {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr);
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--line);
}
.ssb-household__person:first-child { border-top: 0; }
.ssb-household__person > :not(.ssb-person) { grid-column: 2; }
.ssb-household__name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  min-block-size: 2rem;
  font-weight: var(--fw-bold);
  color: var(--text-strong);
}
.ssb-household__name .ssb-state { margin-inline-start: auto; }
.ssb-household__child { font-weight: var(--fw-medium); color: var(--text-body); font-size: var(--text-sm); }
.ssb-household__note { margin: 0; font-size: var(--text-sm); color: var(--text-body); }
.ssb-household__details { display: flex; flex-direction: column; gap: var(--space-2); }
.ssb-household__details strong { color: var(--text-strong); }
.ssb-household__board { margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--line); }

/* The way into a child's log: the one thing on My household to act on, so
   the one outline. It darkens under a finger, as a full-width row does. */
.ssb-household__log {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  inline-size: 100%;
  min-block-size: var(--touch-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-strong);
  font: inherit;
  text-align: start;
  cursor: pointer;
  transition: background-color var(--dur-press) var(--ease-out);
}
.ssb-household__log > span { flex: 1; display: flex; flex-direction: column; }
.ssb-household__log small { font-size: var(--text-sm); color: var(--text-body); font-variant-numeric: tabular-nums; }
.ssb-household__log:active { background: var(--surface-press); }
@media (hover: hover) and (pointer: fine) {
  .ssb-household__log:hover { background: var(--surface-hover); }
}

/* The log: the day in the display face, a ruled line down the visits. */
.ssb-log { display: flex; flex-direction: column; gap: var(--space-2); }
.ssb-log__day { display: grid; grid-template-columns: 3rem minmax(0, 1fr); gap: var(--space-3); align-items: start; }
.ssb-log__visits {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-1) 0 var(--space-4) var(--space-3);
  border-left: 2px solid var(--line-strong);
}
.ssb-log__visit { display: flex; flex-direction: column; gap: var(--space-2); }
.ssb-log__line {
  display: grid;
  grid-template-columns: 1.25rem minmax(0, 1fr);
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-strong);
}
.ssb-log__line .ssb-icon { color: var(--text-body); }
.ssb-log__line--tap .ssb-icon { color: var(--accent); }
.ssb-log__time { font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; }
.ssb-log__slot { display: block; color: var(--text-body); font-variant-numeric: tabular-nums; }
.ssb-log__line--cancelled .ssb-log__what { text-decoration: line-through; color: var(--text-muted); }
/* The log's Details: the chevron turns when it is open. Instant under reduced motion. */
.ssb-household__toggle .ssb-icon { transition: transform var(--dur-fast) var(--ease-out); }
.ssb-household__toggle[aria-expanded="true"] .ssb-icon { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .ssb-household__toggle .ssb-icon { transition: none; } }
/* ---------------------------------------------------------------------------
   Where residents' replies go (design/flows/contact-address.md)
   --------------------------------------------------------------------------- */

/* An address breaks after its @ and nowhere else, so a typo stays readable. */
.ssb-addr { white-space: nowrap; }

/* A state card: the state owns one end of the card (a band on a phone, a
   column from 640px), its icon in a disc of the state's line colour and its
   word in the state's ink; the edge is the state's line. Taste note 14. */
.ssb-statecard {
  --s-fill: var(--ok-fill);
  --s-line: var(--ok-line);
  --s-text: var(--ok-text);
  display: grid;
  max-width: 35rem;
  border: 2px solid var(--s-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
}
.ssb-statecard--attention { --s-fill: var(--attention-fill); --s-line: var(--attention-line); --s-text: var(--attention-text); }
.ssb-statecard__state {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background: var(--s-fill);
  color: var(--s-text);
  font-weight: var(--fw-bold);
}
.ssb-statecard__disc {
  display: grid;
  place-items: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--s-line);
  color: var(--on-accent);
}
.ssb-statecard__main { display: grid; gap: var(--space-1); padding: var(--space-3) var(--space-4) var(--space-4); }
.ssb-statecard__kicker {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: var(--fw-bold);
  color: var(--text-muted);
}
.ssb-statecard__value {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  color: var(--text-strong);
}
.ssb-statecard__value--long { font-size: var(--text-lg); }
.ssb-statecard__sub { margin: 0; font-size: var(--text-sm); color: var(--text-body); }
.ssb-statecard--attention .ssb-statecard__sub { font-size: var(--text-md); font-weight: var(--fw-medium); color: var(--attention-text); }
.ssb-statecard__acts { display: grid; border-top: 2px solid var(--s-line); }
.ssb-statecard__acts > .ssb-btn { justify-content: center; min-height: var(--touch-min); border: 0; border-radius: 0; }
/* A full-width bar darkens on press rather than scaling. */
.ssb-statecard__acts > .ssb-btn:active:not(:disabled) { transform: none; background: var(--surface-press); }
.ssb-statecard--attention .ssb-statecard__acts > .ssb-btn { color: var(--accent); font-weight: var(--fw-bold); }
@media (min-width: 640px) {
  .ssb-statecard { grid-template-columns: 7rem 1fr; grid-template-rows: 1fr auto; }
  .ssb-statecard__state { grid-row: 1 / 3; flex-direction: column; justify-content: center; padding: var(--space-4) var(--space-2); }
  .ssb-statecard__acts { grid-column: 2; border-top: 1px solid var(--line); }
}

/* The letter: an email as the resident's mail app shows it. Both endings
   have the height of the longer, so nothing under it moves while typing. */
.ssb-letter {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-app);
}
.ssb-letter__meta {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--line);
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.ssb-letter__meta b { min-width: 0; font-weight: var(--fw-bold); color: var(--text-strong); }
.ssb-letter__heading { margin: 0; font-family: var(--font-display); font-size: var(--text-lg); font-weight: var(--fw-bold); line-height: var(--lh-snug); color: var(--text-strong); }
.ssb-letter__body { margin: 0; font-size: var(--text-sm); color: var(--text-body); font-variant-numeric: tabular-nums; }
.ssb-letter__last {
  margin: 0;
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
  font-size: var(--text-sm);
  line-height: var(--lh-normal);
  min-height: calc(3em * var(--lh-normal) + var(--space-2));
  color: var(--text-body);
}
.ssb-letter__last strong { color: var(--accent); font-weight: var(--fw-bold); }

/* A field that warns without refusing: allowed, but think twice. */
.ssb-field--warn .ssb-field__control { border-color: var(--attention-edge); }
.ssb-field--warn .ssb-field__status,
.ssb-field--warn .ssb-field__line { color: var(--attention-text); }

/* The sheet: its parts apart by one step; the button never moves, what
   changes is said under it. */
.ssb-replies { display: grid; gap: var(--space-4); }
.ssb-replies__title { margin: 0; font-family: var(--font-display); font-size: var(--text-xl); line-height: var(--lh-snug); color: var(--text-strong); }
.ssb-replies__label { margin: 0; font-size: var(--text-sm); font-weight: var(--fw-bold); color: var(--text-strong); }
.ssb-replies__foot { display: grid; gap: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--line); }
.ssb-replies__foot > .ssb-btn--primary[aria-disabled="true"] { cursor: progress; }
.ssb-replies__failed { display: flex; gap: var(--space-1); align-items: center; margin: 0; font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--danger-text); }
.ssb-replies__failed:empty { display: none; }
.ssb-replies__remove { justify-self: center; color: var(--danger-text); }
.ssb-replies__quiet { margin: 0; font-size: var(--text-sm); color: var(--text-muted); }
.ssb-replies__confirm { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }

/* One line on Notices: where replies go, and the way to change it. */
.ssb-replies-line {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 2px var(--space-2);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}
.ssb-replies-line__lead { display: inline-flex; gap: var(--space-1); align-items: center; color: var(--text-body); }
.ssb-replies-line__value { grid-column: 1; padding-left: 1.375rem; color: var(--text-strong); }
.ssb-replies-line__act { grid-row: 1 / 3; grid-column: 2; }
.ssb-replies-line > .ssb-state + .ssb-replies-line__act { grid-row: auto; }
.ssb-replies-row__value { color: var(--text-strong); }

/* ---------- A space's face ---------------------------------
   Its colour family, picked by data-tone (spaceTone() in ui.js): the board's
   choice in Admin -> Spaces, else its kind's, decided by the server. Worn by
   a tab, the space card, a space button on the wall screen and in
   administration, and a colour swatch. design/flows/space-colour.md. */
[data-tone="teal"]     { --k-solid: var(--space-teal-solid);     --k-tint: var(--space-teal-tint);     --k-ink: var(--space-teal-ink); }
[data-tone="plum"]     { --k-solid: var(--space-plum-solid);     --k-tint: var(--space-plum-tint);     --k-ink: var(--space-plum-ink); }
[data-tone="indigo"]   { --k-solid: var(--space-indigo-solid);   --k-tint: var(--space-indigo-tint);   --k-ink: var(--space-indigo-ink); }
[data-tone="purple"]   { --k-solid: var(--space-purple-solid);   --k-tint: var(--space-purple-tint);   --k-ink: var(--space-purple-ink); }
[data-tone="graphite"] { --k-solid: var(--space-graphite-solid); --k-tint: var(--space-graphite-tint); --k-ink: var(--space-graphite-ink); }
[data-tone="olive"]    { --k-solid: var(--space-olive-solid);    --k-tint: var(--space-olive-tint);    --k-ink: var(--space-olive-ink); }
[data-tone="bark"]     { --k-solid: var(--space-bark-solid);     --k-tint: var(--space-bark-tint);     --k-ink: var(--space-bark-ink); }

/* ---------- A space's colour, chosen -----------------------
   colourPicker() in ui.js. Seven swatches, each the space in miniature: a
   36px tile in a 44px touch box, its mark in white. Seven boxes are 308px,
   inside a card at 375px; under 360px they wrap four and three. The chosen
   one has a heavy outline in its own colour that grows in; no tick. The
   input is invisible, so the tile wears the focus ring, outside the outline.
   design/flows/space-colour.md. */
.ssb-swatches { --swatch-tile: 36px; border: 0; margin: 0; padding: 0; min-width: 0; }
.ssb-swatches__legend { padding: 0; margin-bottom: var(--space-2); font-weight: var(--fw-bold); color: var(--text-strong); }
/* One line, packed from the left, a set gap apart; on a phone the gap shrinks
   to what the set (--n, from ui.js) fits in, and at 360px the touch boxes
   give up the last fraction of a pixel rather than one swatch wrapping alone.
   Never spread wide on a desktop. Pulled left by the touch target's margin so
   the first tile's edge sits on the legend's and the "In use" line's. */
.ssb-swatches__row {
  display: grid;
  grid-template-columns: repeat(var(--n), minmax(0, var(--touch-min)));
  column-gap: max(0px, min(var(--space-3), calc((100% - var(--n) * var(--touch-min)) / (var(--n) - 1))));
  row-gap: var(--space-2);
  margin-inline-start: calc((var(--swatch-tile) - var(--touch-min)) / 2);
}
.ssb-swatch {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--touch-min);
  max-width: 100%;
  height: var(--touch-min);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.ssb-swatch__input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
.ssb-swatch__tile {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--swatch-tile);
  height: var(--swatch-tile);
  border-radius: var(--radius);
  background: var(--k-solid);
  color: var(--on-kind);
  pointer-events: none;
  transition: transform var(--dur-press) var(--ease-out);
}
.ssb-swatch__tile .ssb-kindmark { width: var(--icon-md); height: var(--icon-md); }
.ssb-swatch__tile .ssb-kindmark__fill { opacity: 0.3; }
/* Chosen: 3px of the page's strongest ink, 3px clear of the tile. Never the
   tile's own hue, which read as a slightly bigger tile (Ivan, 2026-10-21). */
.ssb-swatch__tile::after {
  content: "";
  position: absolute;
  inset: -6px;
  border: 3px solid var(--text-strong);
  border-radius: var(--radius-lg);
  opacity: 0;
  transform: scale(0.82);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.ssb-swatch__input:checked + .ssb-swatch__tile::after { opacity: 1; transform: none; }
.ssb-swatch__input:active + .ssb-swatch__tile { transform: scale(0.94); transition-duration: 80ms; }
.ssb-swatch__input:focus-visible + .ssb-swatch__tile {
  outline: var(--ring-width) solid var(--ring-color);
  outline-offset: calc(6px + var(--ring-gap));
}
@media (max-width: 359px) {
  .ssb-swatches__row { display: flex; flex-wrap: wrap; column-gap: var(--space-2); }
}
@media (prefers-reduced-motion: reduce) {
  .ssb-swatch__tile::after { transform: none; }
}
/* One line, two lines' room, so a clash said or unsaid moves nothing below. */
.ssb-swatches__hint {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  min-height: 2lh;
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
/* Who wears what, before any tap: a dot of each colour in use and the
   spaces wearing it; the chosen colour's owners in bold ("Hobby room too"). */
.ssb-swatches__hint--use { flex-wrap: wrap; column-gap: var(--space-3); row-gap: var(--space-1); color: var(--text-body); }
.ssb-swatches__in-use { color: var(--text-muted); }
.ssb-swatches__use { display: inline-flex; align-items: flex-start; gap: var(--space-1); }
.ssb-swatches__use--chosen { font-weight: var(--fw-bold); color: var(--text-strong); }
.ssb-swatches__dot {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  border-radius: var(--radius-sm);
  background: var(--k-solid);
}

/* A kind's mark, in two tones: a soft fill under the line. */
.ssb-kindmark { width: var(--icon-lg); height: var(--icon-lg); flex-shrink: 0; overflow: visible; }
.ssb-kindmark__fill { fill: currentColor; opacity: 0.22; }
.ssb-kindmark__line { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* A space button (wall screen, administration): its mark in its ink; chosen
   is solid in its colour, white words, bold. */
.ssb-spacebtn > .ssb-icon { color: var(--k-ink); }
.ssb-spacebtn--chosen {
  background: var(--k-solid);
  border-color: var(--k-solid);
  color: var(--on-kind);
  font-weight: var(--fw-bold);
}
.ssb-spacebtn--chosen > .ssb-icon { color: var(--on-kind); }

/* ---------- The space tabs ---------------------------------
   Up to four share the row equally (minmax(0, 1fr): a long name must not
   widen its column, or the neck under it lands short). The chosen one is
   solid in its colour with a heavy edge, and its bottom is square, because
   the space card's neck rises into it. */
.ssb-tabs {
  --n: 1; /* how many tabs: set by spaceTabs() */
  display: grid;
  grid-template-columns: repeat(var(--n), minmax(0, 1fr));
  gap: var(--space-2);
}
.ssb-tab {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 0;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text-body);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-press) var(--ease-out);
}
/* Let go, it springs back. Only where linear() is understood: elsewhere the
   curve would void the whole transition, colours included. */
@supports (transition-timing-function: linear(0, 1)) {
  .ssb-tab { transition-duration: var(--dur-fast), var(--dur-fast), var(--dur-spring); transition-timing-function: var(--ease), var(--ease), var(--ease-spring); }
}
/* In quickly under the finger. */
.ssb-tab:active {
  transform: scale(var(--press-scale));
  transition-duration: var(--dur-fast), var(--dur-fast), var(--dur-press);
  transition-timing-function: var(--ease), var(--ease), var(--ease-out);
}
.ssb-tab__band {
  display: flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-2);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--k-tint);
  color: var(--k-ink);
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.ssb-tab__band .ssb-kindmark { width: var(--icon-xl); height: var(--icon-xl); }
.ssb-tab__name {
  /* Four across a 375px phone leaves a tab about 80px: "Laundry" in bold
     must fit on its line whole, so the sides are tight. A word that does not
     fit steps the row down a size (.ssb-tabs--snug), and one too wide even
     then is given hyphens (spaceTabs). Break-word only until that has been
     measured: Chrome prefers it to those hyphens and splits the word
     without one. Margin, not padding: in a box clamped to two lines,
     padding shows the top of a third. */
  margin: var(--space-2) var(--space-2) var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
  overflow-wrap: break-word;
  hyphens: auto;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.ssb-tabs--fitted .ssb-tab__name { overflow-wrap: normal; }
.ssb-tabs--snug .ssb-tab__name { font-size: var(--text-xs); }
.ssb-tab[aria-pressed="true"] {
  border: 3px solid var(--k-solid);
  background: var(--k-solid);
  color: var(--on-kind);
}
.ssb-tab[aria-pressed="true"] .ssb-tab__band { background: var(--k-solid); color: var(--on-kind); }
.ssb-tab[aria-pressed="true"] .ssb-tab__name { font-weight: var(--fw-bold); }
.ssb-tab[aria-pressed="true"] .ssb-kindmark__fill { opacity: 0.3; }
.ssb-tabs:not(.ssb-tabs--scroll) .ssb-tab[aria-pressed="true"] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
@media (hover: hover) and (pointer: fine) {
  .ssb-tab:hover:not([aria-pressed="true"]) { border-color: var(--k-ink); }
}

/* Five or more: one row that scrolls, a fade on the side that has more. */
.ssb-tabs--scroll {
  --fade-left: black;
  --fade-right: black;
  display: flex;
  overflow-x: auto;
  scrollbar-width: none;
  /* A row that scrolls sideways clips up and down as well: room for the
     focus ring above and below, given back so the row does not grow. */
  padding-block: calc(var(--ring-gap) + var(--ring-width));
  margin-block: calc(-1 * (var(--ring-gap) + var(--ring-width)));
  -webkit-mask-image: linear-gradient(to right, var(--fade-left), black var(--space-7), black calc(100% - var(--space-7)), var(--fade-right));
  mask-image: linear-gradient(to right, var(--fade-left), black var(--space-7), black calc(100% - var(--space-7)), var(--fade-right));
}
.ssb-tabs--scroll::-webkit-scrollbar { display: none; }
.ssb-tabs--scroll .ssb-tab { flex: 0 0 calc((100% - var(--space-2) * 2.5) / 3.5); }
.ssb-tabs--more-left { --fade-left: transparent; }
.ssb-tabs--more-right { --fade-right: transparent; }

/* ---------- The spaces on a laptop -------------------------
   From 900px the resident app shows its spaces as a list on the left
   (.ssb-tabs--side) and the chosen space as a frame in its colour round its
   own calendar (.ssb-spaceframe). The chosen row runs into the frame: square
   on that side, over the frame's 3px edge, with a curve above and below
   where they meet, so the space and its calendar read as one thing
   (design/flows/laptop-layout.md). Each row keeps the tab's own looks. */
@media (min-width: 900px) {
  .ssb-tabs.ssb-tabs--side {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    overflow: visible;
    padding: 0;
    margin: 0;
    -webkit-mask-image: none;
    mask-image: none;
  }
  .ssb-tabs--side .ssb-tab {
    flex: none;
    flex-direction: row;
    align-items: stretch;
    min-height: calc(var(--touch-min) + var(--space-3));
    margin-right: var(--space-4);
  }
  .ssb-tabs--side .ssb-tab__band {
    padding: 0 var(--space-3);
    border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  }
  /* Margin, not padding, round the name: in a box clamped to two lines,
     padding shows the top of a third. */
  .ssb-tabs--side .ssb-tab__name {
    align-self: center;
    padding: 0;
    margin: var(--space-2) var(--space-3);
    font-size: var(--text-md);
  }
  .ssb-tabs--side.ssb-tabs--snug .ssb-tab__name { font-size: var(--text-sm); }
  /* Positioned, so it is drawn over the frame's edge that follows it. */
  .ssb-tabs.ssb-tabs--side .ssb-tab[aria-pressed="true"] {
    position: relative;
    margin-right: -3px;
    border-right: 0;
    border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  }
  /* Both curves on one ::before, the full height of the row and a curve
     beyond each end: ::after is the focus ring's (base.css). */
  .ssb-tabs--side .ssb-tab[aria-pressed="true"]::before {
    --curve-top: radial-gradient(circle at 0 0, transparent calc(var(--radius-lg) - 0.5px), var(--k-solid) var(--radius-lg)) top / var(--radius-lg) var(--radius-lg) no-repeat;
    content: "";
    position: absolute;
    right: 0;
    top: calc(-1 * (var(--radius-lg) + 3px));
    bottom: calc(-1 * (var(--radius-lg) + 3px));
    width: var(--radius-lg);
    pointer-events: none;
    background:
      var(--curve-top),
      radial-gradient(circle at 0 100%, transparent calc(var(--radius-lg) - 0.5px), var(--k-solid) var(--radius-lg)) bottom / var(--radius-lg) var(--radius-lg) no-repeat;
  }
  /* The first row, chosen, is flush with the frame's top: no curve above
     (app.css squares the frame's corner there). */
  .ssb-tabs--side .ssb-tab:first-child[aria-pressed="true"]::before { --curve-top: linear-gradient(transparent, transparent); }

  .ssb-spaceframe {
    border: 3px solid var(--k-solid, var(--line-strong));
    border-radius: var(--radius-lg);
    background: var(--surface);
    overflow: clip;
  }
}

/* ---------- The space card ----------------------------------
   Head: the mark and the name, white on the kind's solid, the loudest
   thing after the date. Then what belongs to the space, then its actions as
   one strip. No border on the head, so a tab that lands on it meets it
   with no line between. */
.ssb-spacecard {
  /* Which tab it hangs from, of how many, and how tall the gap above it is:
     set by spaceCard() and by the page that places it. */
  --i: 0;
  --n: 1;
  --gap-above: var(--space-3);
  position: relative;
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.ssb-spacecard__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: calc(var(--touch-min) * 2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--k-solid);
  color: var(--on-kind);
}
.ssb-spacecard__head .ssb-kindmark { width: var(--icon-2xl); height: var(--icon-2xl); }
.ssb-spacecard__head .ssb-kindmark__fill { opacity: 0.3; }
.ssb-spacecard__name {
  min-width: 0;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--fw-heavy);
  letter-spacing: var(--tracking-tight);
  line-height: var(--lh-tight);
  color: inherit;
  overflow-wrap: anywhere;
}
/* Everything under the head is framed; the head is not. */
.ssb-spacecard > :not(.ssb-spacecard__head) {
  border-left: 1px solid var(--line);
  border-right: 1px solid var(--line);
}
.ssb-spacecard > :last-child:not(.ssb-spacecard__head) {
  border-bottom: 1px solid var(--line);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.ssb-spacecard > .ssb-spacecard__head:last-child { border-radius: var(--radius-lg); }
.ssb-spacecard__actions {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  border-top: 1px solid var(--line);
  overflow: hidden;
}
.ssb-spacecard__action {
  --ring-gap: calc(-1 * var(--ring-width));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding: 0 var(--space-2);
  border: 0;
  background: none;
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--text-body);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}
.ssb-spacecard__action + .ssb-spacecard__action { border-left: 1px solid var(--line); }
.ssb-spacecard__action:active { background: var(--surface-press); }
@media (hover: hover) and (pointer: fine) {
  .ssb-spacecard__action:hover { background: var(--surface-hover); }
}

/* The neck: the card rises into the gap above it to meet the chosen tab,
   with a curve each side where it has a neighbour. Its column is worked out
   from the tabs' own numbers (--i of --n, the same gap), so it is exactly
   the tab's width; it overlaps the tab's 3px bottom edge, same colour. */
.ssb-spacecard--joined::before {
  --col: calc((100% - var(--space-2) * (var(--n) - 1)) / var(--n));
  --curve: var(--radius-lg);
  content: "";
  position: absolute;
  bottom: 100%;
  left: calc((var(--col) + var(--space-2)) * var(--i) - var(--curve));
  width: calc(var(--col) + var(--curve) * 2);
  height: calc(var(--gap-above) + 3px);
  pointer-events: none;
  background:
    linear-gradient(var(--k-solid), var(--k-solid)) center / calc(100% - var(--curve) * 2) 100% no-repeat,
    radial-gradient(circle at 0 0, transparent calc(var(--curve) - 0.5px), var(--k-solid) var(--curve)) left bottom / var(--curve) var(--curve) no-repeat,
    radial-gradient(circle at 100% 0, transparent calc(var(--curve) - 0.5px), var(--k-solid) var(--curve)) right bottom / var(--curve) var(--curve) no-repeat;
}
.ssb-spacecard--first::before {
  left: 0;
  width: calc(var(--col) + var(--curve));
  background:
    linear-gradient(var(--k-solid), var(--k-solid)) left / calc(100% - var(--curve)) 100% no-repeat,
    radial-gradient(circle at 100% 0, transparent calc(var(--curve) - 0.5px), var(--k-solid) var(--curve)) right bottom / var(--curve) var(--curve) no-repeat;
}
.ssb-spacecard--last::before {
  width: calc(var(--col) + var(--curve));
  background:
    linear-gradient(var(--k-solid), var(--k-solid)) right / calc(100% - var(--curve)) 100% no-repeat,
    radial-gradient(circle at 0 0, transparent calc(var(--curve) - 0.5px), var(--k-solid) var(--curve)) left bottom / var(--curve) var(--curve) no-repeat;
}
/* Where the tab lands on a corner, the corner is square. */
.ssb-spacecard--first .ssb-spacecard__head { border-top-left-radius: 0; }
.ssb-spacecard--last .ssb-spacecard__head { border-top-right-radius: 0; }

/* ---------- The building, and who is signed in ---------------
   The top of the resident app (buildingHeader in ui.js): the building's
   photo, or the brand's blue, with its name on it as the page's heading,
   and its two top corners cut out, the Maretou mark in one and the account
   in the other. Ivan's sketch of 2026-10-21 (design/flows/resident-header.md).
   The cuts are drawn in the page's own colour over the photo, so the photo
   seems to bend round them: an even gap, a corner concentric with the
   mark's, and one soft curve each side where the photo turns. Nothing is
   framed and nothing has a shadow. */
.ssb-hero {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: var(--hero-height);
  /* The name sits under the corners, never beside them: a long one grows
     the header downwards. */
  padding: calc(var(--hero-mark) + var(--hero-gap) + var(--space-3)) clamp(var(--space-4), 2vw, var(--space-6)) clamp(var(--space-3), 1.6vw, var(--space-5));
}
.ssb-hero__ground {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: var(--radius-hero);
  background: var(--brand-navy);
}
.ssb-hero__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 40%;
}
/* The board's own framing: already cut to the header's shape, so centred. */
.ssb-hero__picture { display: contents; }
.ssb-hero__picture .ssb-hero__photo { object-position: 50% 50%; }
.ssb-hero__ground::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--hero-shade);
}
/* No photo: the brand's blue, and the mark's skyline standing faintly in it. */
.ssb-hero__ground--plain { background: var(--hero-plain); }
.ssb-hero__ground--plain::after { content: none; }
/* On a phone it stands small in the bottom corner, below the account's
   corner; at a desk the header is wide and it stands taller. */
.ssb-hero__skyline {
  position: absolute;
  right: 4%;
  bottom: 0;
  height: 42%;
  width: auto;
  aspect-ratio: 1;
  fill: var(--brand-white);
  opacity: 0.12;
}

.ssb-hero__cut {
  --curve: var(--radius-hero);
  position: absolute;
  top: -1px;
  display: flex;
  background: var(--bg-app);
}
.ssb-hero__cut::before,
.ssb-hero__cut::after {
  content: "";
  position: absolute;
  width: var(--curve);
  height: var(--curve);
  pointer-events: none;
}
.ssb-hero__cut--mark {
  left: -1px;
  padding: 1px var(--hero-gap) var(--hero-gap) 1px;
  border-bottom-right-radius: var(--radius-hero-cut);
}
.ssb-hero__cut--mark::before,
.ssb-hero__cut--mark::after {
  background: radial-gradient(circle at 100% 100%, transparent calc(var(--curve) - 0.5px), var(--bg-app) var(--curve));
}
.ssb-hero__cut--mark::before { left: 0; top: calc(100% - 1px); }
.ssb-hero__cut--mark::after { left: calc(100% - 1px); top: 0; }
.ssb-hero__cut--account {
  right: -1px;
  padding: 1px 1px var(--hero-gap) var(--hero-gap);
  border-bottom-left-radius: var(--radius-hero-cut);
}
.ssb-hero__cut--account::before,
.ssb-hero__cut--account::after {
  background: radial-gradient(circle at 0 100%, transparent calc(var(--curve) - 0.5px), var(--bg-app) var(--curve));
}
.ssb-hero__cut--account::before { right: 0; top: calc(100% - 1px); }
.ssb-hero__cut--account::after { right: calc(100% - 1px); top: 0; }

.ssb-brand {
  display: block;
  width: var(--hero-mark);
  height: var(--hero-mark);
}
.ssb-brand__tile { fill: var(--brand-blue); }
.ssb-brand__wall { fill: var(--brand-white); stroke: var(--brand-white); stroke-width: 2; stroke-linejoin: round; }
.ssb-brand__dark { fill: var(--brand-window); }
.ssb-brand__lit { fill: var(--brand-amber); }
.ssb-brand__word {
  fill: var(--brand-navy);
  font-family: var(--font-display);
  font-weight: var(--fw-heavy);
  letter-spacing: var(--tracking-brand);
}

/* White on the photo in both themes: the shade under it is the same. */
.ssb-hero__name {
  position: relative;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-hero);
  font-weight: var(--fw-heavy);
  line-height: var(--lh-hero);
  letter-spacing: var(--tracking-hero);
  color: var(--brand-white);
  text-shadow: var(--hero-text-shadow);
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  padding-bottom: calc(0.3em + var(--space-1));
}
/* A short amber rule under the name, the amber of the mark's windows. */
.ssb-hero__name::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--hero-rule, clamp(2.25rem, 3vw + 1.5rem, 4rem));
  height: 0.1em;
  border-radius: var(--radius-plate);
  background: var(--brand-amber);
}
/* At a desk the calendar must still start on the first screen
   (design/flows/laptop-layout.md), so the name moves up beside the mark,
   between the two corners, and the header is only as tall as a corner and a
   strip of photo under it. */
@media (min-width: 900px) {
  .ssb-hero:not(.ssb-hero--phone) {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: end;
    min-height: 0;
    padding: 0;
  }
  .ssb-hero:not(.ssb-hero--phone) .ssb-hero__cut { position: relative; top: auto; left: auto; right: auto; align-self: start; margin-top: -1px; }
  .ssb-hero:not(.ssb-hero--phone) .ssb-hero__cut--mark { grid-column: 1; grid-row: 1; margin-left: -1px; margin-bottom: var(--space-4); }
  .ssb-hero:not(.ssb-hero--phone) .ssb-hero__cut--account { grid-column: 3; grid-row: 1; margin-right: -1px; }
  .ssb-hero:not(.ssb-hero--phone) .ssb-hero__name {
    grid-column: 2;
    grid-row: 1;
    margin: 0 0 var(--space-3);
    padding-left: calc(var(--radius-hero) + var(--space-3));
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }
  .ssb-hero:not(.ssb-hero--phone) .ssb-hero__name::after { left: calc(var(--radius-hero) + var(--space-3)); }
  /* Clear of the account's corner, however long the apartment's label. */
  .ssb-hero:not(.ssb-hero--phone) .ssb-hero__skyline { right: auto; left: 55%; height: 86%; }
}
/* A header drawn at one device's size whatever the window: the board's photo
   framer (design/flows/building-photo.md) shows the phone header and the
   laptop header side by side, each laid out at its true pixels and scaled
   to fit, so the tokens the window would choose are fixed here: a 375
   window's for the phone, a 1440 window's for the laptop. */
.ssb-hero--phone {
  --hero-mark: 72px;
  --hero-gap: 12px;
  --hero-height: 168px;
  --text-hero: 2rem;
  --text-hero-apartment: 1.375rem;
  --hero-rule: 2.25rem;
}
.ssb-hero--desk {
  --hero-mark: 78px;
  --hero-gap: 14px;
  --text-hero: 3.75rem;
  --text-hero-apartment: 1.875rem;
  --hero-rule: 4rem;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: end;
  min-height: 0;
  padding: 0;
}
.ssb-hero--desk .ssb-hero__cut { position: relative; top: auto; left: auto; right: auto; align-self: start; margin-top: -1px; }
.ssb-hero--desk .ssb-hero__cut--mark { grid-column: 1; grid-row: 1; margin-left: -1px; margin-bottom: var(--space-4); }
.ssb-hero--desk .ssb-hero__cut--account { grid-column: 3; grid-row: 1; margin-right: -1px; }
.ssb-hero--desk .ssb-hero__name {
  grid-column: 2;
  grid-row: 1;
  margin: 0 0 var(--space-3);
  padding-left: calc(var(--radius-hero) + var(--space-3));
  -webkit-line-clamp: 2;
  line-clamp: 2;
}
.ssb-hero--desk .ssb-hero__name::after { left: calc(var(--radius-hero) + var(--space-3)); }
.ssb-hero--desk .ssb-hero__skyline { right: auto; left: 55%; height: 86%; }
.ssb-nowrap { white-space: nowrap; }

.ssb-account {
  display: inline-flex;
  align-items: stretch;
  flex-shrink: 0;
  /* As tall as the mark across from it, with the same corner. */
  height: var(--hero-mark);
  padding: 0;
  border: 2px solid var(--line-strong);
  border-radius: var(--radius-hero-mark);
  background: var(--surface);
  color: var(--text-strong);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-press) var(--ease-out);
}
.ssb-account:active { transform: scale(var(--press-scale)); background: var(--surface-press); }
/* The apartment: its house over its number. */
.ssb-account__who {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-width: 0;
  padding: 0 clamp(var(--space-3), 1vw + var(--space-2), var(--space-5));
  font-family: var(--font-display);
  font-size: var(--text-hero-apartment);
  font-weight: var(--fw-heavy);
  line-height: var(--lh-tight);
}
/* An apartment label can be "Penthouse North 12": the button keeps to its
   corner and the label ends in "…". */
.ssb-account__who > span {
  max-width: 5.5rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.ssb-account--signin .ssb-account__who { font-size: var(--text-md); }
.ssb-account__menu {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--hero-mark);
}
/* A short divider, not a full-height wall, between the two halves. */
.ssb-account__menu::before {
  content: "";
  position: absolute;
  left: 0;
  top: 22%;
  bottom: 22%;
  border-left: 2px solid var(--line);
}
.ssb-account__who .ssb-icon { width: clamp(20px, 0.8vw + 16px, 26px); height: clamp(20px, 0.8vw + 16px, 26px); }
.ssb-account__menu .ssb-icon { width: clamp(28px, 1vw + 24px, 38px); height: clamp(28px, 1vw + 24px, 38px); }
.ssb-account[aria-expanded="true"] { background: var(--accent-tint); border-color: var(--accent); }
.ssb-account[aria-expanded="true"] .ssb-account__menu::before { border-left-color: var(--accent); }
/* At a desk the menu hangs from the button: the button's foot opens into it.
   Joined, it is at full size at once: a click let go of after a long press
   still has the press easing back out, and openSheet() measures the button
   the moment the menu opens, so the menu would hang from a button a few
   pixels smaller than the one it then sits under. */
.ssb-account[data-joined] {
  transform: none;
  transition-property: background-color, border-color;
  background: var(--surface);
  border-bottom-color: var(--surface);
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
@media (hover: hover) and (pointer: fine) {
  .ssb-account:hover { border-color: var(--text-muted); }
}

/* ---------- The sheet, from the side and hung from a button --
   The account menu. On a phone it comes in from the right edge, where its
   button is, full height, and a finger pushes it back right: the handle on
   its left edge says so. At a desk it hangs from its button, joined to it:
   the same 2px edge, the button's foot open into the menu's top. */
.ssb-sheet-backdrop--menu { justify-content: flex-end; align-items: stretch; }
.ssb-sheet--menu {
  --sheet-from: translateX(100%);
  --sheet-swipe: x;
  display: flex;
  flex-direction: column;
  width: min(20rem, 85vw);
  max-width: none;
  max-height: none;
  height: 100dvh;
  border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  padding: var(--space-3) var(--space-4) calc(var(--space-5) + env(safe-area-inset-bottom, 0px)) var(--space-5);
}
.ssb-sheet--menu::before {
  content: "";
  position: absolute;
  left: var(--space-2);
  top: 50%;
  width: var(--space-1);
  height: calc(var(--space-7) * 1.5);
  margin-top: calc(var(--space-7) * -0.75);
  border-radius: var(--radius-sm);
  background: var(--text-muted);
}
.ssb-sheet-backdrop[data-dragging] .ssb-sheet--menu { transform: translateX(var(--sheet-drag)); }
@media (prefers-reduced-motion: reduce) {
  .ssb-sheet--menu { --sheet-from: translateX(var(--sheet-drag)); }
}

.ssb-sheet-backdrop--anchor {
  /* Where the button it hangs from ends: set by openSheet(). */
  --anchor-top: 0px;
  --anchor-right: 0px;
  display: block;
}
.ssb-sheet-backdrop--anchor::before { background: transparent; }
.ssb-sheet--anchor {
  --sheet-from: translateY(calc(-1 * var(--space-2)));
  --sheet-from-opacity: 0;
  --sheet-dur: var(--dur-base);
  --sheet-ease: var(--ease-out);
  --sheet-swipe: 0;
  position: absolute;
  top: calc(var(--anchor-top) - 2px);
  right: var(--anchor-right);
  width: 18rem;
  max-width: calc(100vw - var(--space-4) * 2);
  padding: var(--space-3) var(--space-4) var(--space-4);
  border: 2px solid var(--accent);
  border-radius: var(--radius-lg) 0 var(--radius-lg) var(--radius-lg);
  box-shadow: var(--shadow-overlay);
}
.ssb-sheet--anchor::before { content: none; }

/* The menu's own rows. */
.ssb-menu__who {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0 var(--space-4);
  border-bottom: 1px solid var(--line);
}
.ssb-menu__mark {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: calc(var(--touch-min) + var(--space-1));
  height: calc(var(--touch-min) + var(--space-1));
  border-radius: var(--radius-lg);
  background: var(--accent-tint);
  color: var(--accent);
}
.ssb-menu__title { margin: 0; font-size: var(--text-md); font-weight: var(--fw-bold); color: var(--text-strong); }
.ssb-menu__sub { margin: 0; font-size: var(--text-sm); color: var(--text-body); }
.ssb-menu__row {
  --ring-gap: calc(-1 * var(--ring-width));
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: calc(var(--touch-min) + var(--space-2));
  padding: 0 var(--space-1);
  border: 0;
  border-bottom: 1px solid var(--line);
  background: none;
  font-size: var(--text-md);
  color: var(--text-strong);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}
.ssb-menu__row:active { background: var(--surface-press); }
@media (hover: hover) and (pointer: fine) {
  .ssb-menu__row:hover { background: var(--surface-hover); color: var(--text-strong); text-decoration: none; }
}
.ssb-menu__row > span { flex: 1; }
/* The page on show: marked, not removed, so the menu has the same rows on
   every page. Tinted, its icon in accent, bold, and said in words. */
.ssb-menu__row--here {
  /* Inside the rules, with room so the words do not touch the tint's end. */
  padding-inline: var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--accent-tint);
  font-weight: var(--fw-bold);
}
.ssb-menu__row--here > .ssb-icon { color: var(--accent); }
.ssb-menu__row > .ssb-menu__here { flex: 0 0 auto; font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--accent); }
/* Leaving looks like leaving: its own ink and the door icon, apart at the
   foot of the menu (in reach of a thumb on a phone). */
.ssb-menu__row--leave {
  margin-top: auto;
  border-top: 1px solid var(--line);
  border-bottom: 0;
  color: var(--danger-text);
  font-weight: var(--fw-bold);
}
.ssb-sheet--anchor .ssb-menu__row--leave { margin-top: 0; }
.ssb-menu__close { align-self: flex-end; }

/* "How it works": the space's rules, one sentence each, with its icon. */
.ssb-howto__name { margin: 0; color: var(--text-body); }
.ssb-howto { list-style: none; margin: var(--space-4) 0; padding: 0; }
.ssb-howto li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--line);
  font-size: var(--text-md);
  color: var(--text-body);
}
.ssb-howto li > .ssb-icon { flex-shrink: 0; color: var(--text-strong); }

/* --------------------------------------------------------------------------
   The building's photo, framed by the board (design/flows/building-photo.md).
   What is dragged is the real header, laid out at a phone's or a laptop's
   size and scaled to the width it has. The ground is the page's own colour,
   the colour the header's corners are cut in, so no corner shows a seam. */
.photo-preview {
  position: relative;
  width: 100%;
  min-width: 0;
  border-radius: var(--radius-hero);
}
/* Laid out at the device's width and scaled down, out of the flow, so its
   true width never widens the page. */
.photo-preview__inner {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
}
/* The phone header is never drawn larger than a phone draws it. */
.photo-preview--phone { max-width: 332px; }
.photo-preview .ssb-hero__photo {
  position: absolute;
  top: 0;
  left: 0;
  max-width: none;
  object-fit: fill;
  will-change: transform;
}
/* A saved picture is already cut to its header: it covers, as the app's does. */
.photo-preview .photo-preview__saved {
  position: static;
  object-fit: cover;
}
.photo-preview__photo--snap {
  transition: transform var(--dur-spring) var(--ease-spring);
}
.photo-preview--drag {
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
/* Held: the heavy outline in the accent grows in, the way every pressed
   thing answers here (design-taste 2). */
.photo-preview--drag::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-hero);
  box-shadow: 0 0 0 3px var(--accent);
  opacity: 0;
  transform: scale(1.02);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
  pointer-events: none;
}
.photo-preview--held { cursor: grabbing; }
/* The laptop's strip is 31px tall on a phone: a finger may start a little
   above or below it. */
.photo-preview--desk.photo-preview--drag::before {
  content: "";
  position: absolute;
  inset: -8px 0;
}
.photo-preview--held::after { opacity: 1; transform: none; }

/* The zoom: a thick track, solid accent up to the thumb. */
.photo-zoom {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  color: var(--text-muted);
}
.photo-zoom__range {
  --fill: 0%;
  flex: 1;
  height: var(--touch-min);
  margin: 0;
  background: transparent;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
}
.photo-zoom__range::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: var(--radius-plate);
  background: linear-gradient(to right, var(--accent) var(--fill), var(--line-strong) var(--fill));
}
.photo-zoom__range::-moz-range-track {
  height: 8px;
  border-radius: var(--radius-plate);
  background: linear-gradient(to right, var(--accent) var(--fill), var(--line-strong) var(--fill));
}
.photo-zoom__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 28px;
  height: 28px;
  margin-top: -10px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--accent);
  box-shadow: var(--shadow-raised);
  transition: transform var(--dur-fast) var(--ease-out);
}
.photo-zoom__range::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--accent);
  transition: transform var(--dur-fast) var(--ease-out);
}
.photo-zoom__range:active::-webkit-slider-thumb { transform: scale(1.15); }
.photo-zoom__range:active::-moz-range-thumb { transform: scale(1.15); }
.photo-zoom__range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--ring-color); }
.photo-zoom__range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px var(--ring-color); }

/* The framer: the whole screen on a phone, so a finger on the photo is never
   the page scrolling; centred at a desk. */
.photo-framer {
  width: 100%;
  max-width: 100%;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--bg-app);
  color: var(--text-body);
  display: none;
  flex-direction: column;
}
.photo-framer[open] {
  display: flex;
  animation: photo-framer-in var(--dur-sheet) var(--ease-drawer);
}
.photo-framer::backdrop { background: rgb(0 0 0 / 0.45); }
@keyframes photo-framer-in { from { transform: translateY(24px); opacity: 0; } }
.photo-framer__body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-4);
  display: grid;
  align-content: start;
  gap: var(--space-5);
}
.photo-framer__heading {
  margin: 0;
  font: var(--fw-bold) var(--text-xl)/1.2 var(--font-display);
  color: var(--text-strong);
}
.photo-framer__view { display: grid; gap: var(--space-2); }
.photo-framer__label {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
}
.photo-framer__title { font-weight: var(--fw-bold); color: var(--text-strong); }
.photo-framer__hint {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-body);
}
.photo-framer__soft,
.photo-framer__problem {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  margin: 0;
  font-size: var(--text-sm);
}
.photo-framer__soft { color: var(--attention-text); }
.photo-framer__problem { color: var(--danger-text); }
.photo-framer__problem:empty { display: none; }
.photo-framer__soft .ssb-icon,
.photo-framer__problem .ssb-icon { flex: none; margin-top: 2px; }
.photo-framer__foot {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: var(--bg-app);
}
.photo-framer__zoom { display: flex; align-items: center; gap: var(--space-3); }
.photo-framer__save[aria-disabled="true"] { cursor: progress; }
.photo-framer__cancel[aria-disabled="true"] { cursor: not-allowed; }
.photo-framer__buttons { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
@media (min-width: 900px) {
  .photo-framer {
    width: min(880px, calc(100vw - 2 * var(--space-6)));
    height: fit-content;
    max-height: calc(100dvh - 2 * var(--space-6));
    margin: auto;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-overlay);
  }
  .photo-framer__body { padding: var(--space-6); }
  .photo-framer__foot {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    padding: var(--space-4) var(--space-6);
  }
  .photo-framer__problem { grid-column: 1 / -1; }
  .photo-framer__view--desk { order: -1; }
  .photo-framer__heading { order: -2; }
  .photo-framer__buttons { grid-template-columns: auto auto; }
}

/* Admin → Building: the photo as residents see it, on a laptop and on a
   phone, and what can be done to it. Empty, the header as it is now, blue. */
.photo-section { display: grid; gap: var(--space-4); padding: var(--space-4); }
.photo-section__views { display: grid; gap: var(--space-4); }
.photo-section__phone { order: 1; }
.photo-section__desk { order: 2; }
.photo-section__side { order: 3; display: grid; gap: var(--space-3); align-content: start; }
.photo-section__views--empty .photo-section__desk { display: none; }
.photo-section__phone,
.photo-section__desk { display: grid; gap: var(--space-2); min-width: 0; }
.photo-section__label { font-size: var(--text-sm); font-weight: var(--fw-bold); color: var(--text-strong); }
.photo-section__confirm { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); margin-top: var(--space-4); }
.photo-section__file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.photo-section__line {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  min-height: calc(2 * 1.45em);
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.photo-section__line > .ssb-icon,
.photo-section__line > .ssb-spinner,
.photo-section__line > .ssb-success { flex: none; margin-top: 1px; }
.photo-section__line--danger { color: var(--danger-text); }
.photo-section__line--done { color: var(--ok-text); font-weight: var(--fw-bold); }
@media (min-width: 900px) {
  .photo-section__views { grid-template-columns: 332px minmax(0, 1fr); column-gap: var(--space-6); }
  .photo-section__desk { order: 1; grid-column: 1 / -1; }
  .photo-section__phone { order: 2; }
  .photo-section__views--empty .photo-section__desk { display: grid; }
  .photo-section__views--empty .photo-section__phone { display: none; }
  .photo-section__views--empty .photo-section__side { grid-column: 1 / -1; grid-template-columns: auto minmax(0, 1fr); align-items: center; }
  .photo-section__side > .ssb-btn { justify-self: start; }
}

/* -------------------------------------------------------------------------
   Places: one small tile per place in a window (ui.js placeTiles).
   A tile in miniature: the tiles' corner family and a fine edge in the
   state's line over the state's fill (Ivan, 2026-10-20). The place that can
   still be taken is the solid one, so what is left is counted at a glance.
   ------------------------------------------------------------------------- */
.ssb-places { display: inline-flex; align-items: center; gap: 4px; flex: none; }
.ssb-place {
  width: 13px;
  height: 17px;
  box-sizing: border-box;
  border: 1.5px solid transparent;
  border-radius: var(--radius-plate);
}
.ssb-places--lg { gap: 6px; }
.ssb-places--lg .ssb-place { width: 30px; height: 30px; border-radius: var(--radius-sm); }
.ssb-place--free {
  background: linear-gradient(to bottom, color-mix(in oklab, var(--free-line) 80%, var(--free-fill)), var(--free-line));
  border-color: var(--free-text);
}
.ssb-place--mine {
  background: linear-gradient(to bottom, color-mix(in oklab, var(--mine-line) 80%, var(--mine-fill)), var(--mine-line));
  border-color: var(--mine-text);
}
.ssb-place--taken { background: var(--taken-fill); border-color: var(--text-muted); }
.ssb-place--cant  { background: var(--surface); border-color: var(--text-body); }
/* Dashed means one thing only: a place closed, or going. */
.ssb-place--gone  { background: var(--surface); border-style: dashed; border-color: var(--text-muted); }

.ssb-places--bar {
  width: 7.5rem;
  height: 17px;
  gap: 0;
  overflow: hidden;
  box-sizing: border-box;
  border: 1.5px solid var(--text-muted);
  border-radius: var(--radius-plate);
  background: var(--taken-fill);
}
.ssb-places--bar.ssb-places--lg { width: 100%; height: 30px; border-radius: var(--radius-sm); }
.ssb-places--bar .ssb-place { width: auto; height: 100%; border: 0; border-radius: 0; }
.ssb-places--bar .ssb-place--free { box-shadow: inset -1.5px 0 0 var(--free-text); }

.ssb-places__count { font-size: var(--text-sm); color: var(--text-body); white-space: nowrap; }
.ssb-places__count b { font-family: var(--font-display); font-weight: var(--fw-bold); color: var(--text-strong); }

/* A window with places, in the day list: the tiles and "2 of 4 available" on
   a line of their own under the time and the name, the state still at the
   row's end. Never wraps: below 15rem the words shorten (calendar.js). */
.ssb-slot__places {
  order: 9;
  flex-basis: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: calc(-1 * var(--space-1));
}

/* In the week: the number of places left is the tile's word. */
.ssb-cell__count {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--free-text);
}
.ssb-cell__count--sm { font-size: var(--text-sm); color: var(--mine-text); }

/* A one-window month: "2 of 4" on a wide screen, "2" in a phone's box. */
.ssb-month__of { display: none; }
@media (min-width: 640px) {
  .ssb-month__of { display: inline; }
}

/* The key beside a space's name: how many places each window has. */
.ssb-spacecard__places {
  margin-left: auto;
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  opacity: 0.85;
  white-space: nowrap;
}

/* -------------------------------------------------------------------------
   Place cards: a named place in a window's sheet, two to a row (four on the
   wall screen). Free ones wear the free wash and edge; the chosen one the
   heavy accent outline, inset so it is not a pixel bigger, and no tick
   (design-taste 2). Taken: grey, the apartment's plate on white.
   ------------------------------------------------------------------------- */
.ssb-placecards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: 1fr;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.ssb-placecard {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-strong);
  font: inherit;
  text-align: left;
  transition: transform var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
button.ssb-placecard { cursor: pointer; }
button.ssb-placecard:active { transform: scale(var(--press-scale)); }
.ssb-placecard__name {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: var(--fw-heavy);
  line-height: var(--lh-tight);
  overflow-wrap: break-word;
}
.ssb-placecard__state {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  font-weight: var(--fw-bold);
}
.ssb-placecard--free {
  border-color: var(--free-line);
  background: linear-gradient(to bottom, var(--free-fill) 35%, color-mix(in oklab, var(--free-fill) 35%, var(--surface)));
}
.ssb-placecard--free .ssb-placecard__state { color: var(--free-text); }
.ssb-placecard--chosen {
  border-color: var(--accent);
  background: var(--free-fill);
  box-shadow: inset 0 0 0 3px var(--accent);
}
.ssb-placecard--mine {
  border-color: var(--mine-line);
  background: linear-gradient(to bottom, var(--mine-fill) 35%, color-mix(in oklab, var(--mine-fill) 35%, var(--surface)));
}
.ssb-placecard--mine .ssb-placecard__state { color: var(--mine-text); }
.ssb-placecard--taken { background: var(--taken-fill); }
.ssb-placecard--taken .ssb-placecard__state { color: var(--taken-text); font-weight: var(--fw-regular); }
.ssb-placecard--taken .ssb-apt { background: var(--surface); box-shadow: inset 0 0 0 1px var(--line-strong); }
.ssb-placecard--closed .ssb-placecard__state,
.ssb-placecard--cant .ssb-placecard__state { color: var(--text-body); font-weight: var(--fw-regular); }
/* Dashed: closed, or going after its last booking. */
.ssb-placecard--closed,
.ssb-placecard--leaving { border-style: dashed; }
@media (prefers-reduced-motion: reduce) {
  .ssb-placecard { transition: background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
  button.ssb-placecard:active { transform: none; }
}
