/* The design system, layer two: components.

   The parts every page is built from - one button, one field, one card, one pill, one
   segmented control, one dialog - each defined once, here, and worn by a page through
   its class. A page sheet may place a component (`.rail .btn { width: 100% }`) and may
   add a modifier of its own that is compound with the component's class
   (`.btn.sd-send`); it does not define the component again under a name of its own.
   That was how the app came to have six segmented controls, five pills, three switches
   and three progress bars, each a pixel or a shade from the others. The test
   (test_the_design_system_holds.py) fails a stylesheet that redefines one of these
   classes on its own.

   Naming follows what the codebase already did most:
     - a component is one short class: .btn, .card, .pill, .seg, .switch, .modal;
     - its parts are the component's name with a hyphen, or a short prefix, inside it:
       .modal-body, .seg-opt, .switch-knob; .chip .code;
     - a variant or a state is an `is-` class on the component: .pill.is-ok,
       .seg-opt.is-on, .btn.is-loading; `has-` says what it contains (.tile.has-icon);
     - colour comes in through a custom property where a component is drawn in a
       colour that is not its own: --tone (tile, stat), --ic (icon chip), --pc
       (avatar, a person's colour), --c (service chip), --k (banner), --seg-on (seg).
       That is what a style attribute may carry: a colour by name, never a style.
     - the older names .btn-primary, .btn-ghost, .btn-danger and .btn-sm are the
       button's variants and sizes; they predate the is- convention and are kept.

   Every state is styled here, so a page does not have to: :hover, :focus-visible
   (the outline from --focus-outline, or --focus-ring on a field), :active, :disabled,
   .is-loading, .is-on / [aria-selected="true"] for the chosen one of several.

   The living style guide (Admin > Site > Design system, generate/styleguide.js) draws
   every one of these, in every state, from this file. */

/* ================================================================ button
   A button, and its variants: the default is the secondary, quiet one; -primary is the
   one thing to do on a page; -ghost has no box until it is pointed at; -danger is a
   thing that cannot be taken back. -sm is the compact size, -lg the large one; .icon
   is a button that is only a glyph (an icon button - give it an aria-label).
   .is-loading keeps its width and swaps its label for a spinner. */
.btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-md); cursor: pointer;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  font-size: var(--fs-sm); font-weight: var(--fw-semi); letter-spacing: .01em;
  transition: background var(--dur-base), border-color var(--dur-base), transform var(--dur-fast), box-shadow var(--dur-base), opacity var(--dur-base);
}
.btn:hover:not(:disabled) { background: var(--surface-3); border-color: color-mix(in srgb, var(--ink-3) 40%, var(--line)); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: .42; cursor: not-allowed; }
.btn.icon { padding: var(--sp-2); }
.btn-ghost { background: transparent; border-color: transparent; }
.btn-ghost:hover:not(:disabled) { background: var(--surface-3); border-color: transparent; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); box-shadow: var(--shadow-sm); }
.btn-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 88%, var(--shade)); border-color: transparent; }
.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); background: color-mix(in srgb, var(--danger) 8%, transparent); }
.btn-sm { padding: var(--sp-2) var(--sp-2); font-size: var(--fs-sm); }
/* A count on a button: the canonical .badge, sized for it. */
.btn .badge { min-width: 17px; height: 17px; padding: 0 var(--sp-1); border-radius: var(--r-pill); display: grid; place-items: center; font-size: var(--fs-2xs); font-weight: var(--fw-bold); background: var(--accent); color: var(--accent-ink); }
.btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
.btn-lg { padding: var(--sp-3) var(--sp-5); font-size: var(--fs-base); border-radius: var(--r-md); }
.btn.icon svg, .btn svg { flex: none; }
.btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 14%, transparent);
  border-color: color-mix(in srgb, var(--danger) 50%, var(--line)); }
.btn.is-loading { position: relative; color: transparent; pointer-events: none; }
.btn.is-loading::after { content: ""; position: absolute; inset: 0; margin: auto;
  width: var(--icon-xs); height: var(--icon-xs); border-radius: 50%;
  border: var(--bw-2) solid color-mix(in srgb, var(--ink) 25%, transparent);
  border-top-color: var(--ink); animation: spin var(--dur-linger) linear infinite; }
.btn-primary.is-loading::after { border-color: color-mix(in srgb, var(--accent-ink) 30%, transparent);
  border-top-color: var(--accent-ink); }

/* A link that is a button: an action that reads as part of a sentence. */
.linkbtn { border: 0; background: none; padding: 0; cursor: pointer; color: var(--accent); font-size: var(--fs-sm); font-weight: var(--fw-semi); letter-spacing: .02em; }
.linkbtn:hover { text-decoration: underline; }
.linkbtn.danger { color: var(--danger); }
/* A link set apart from what is above it, by the gap a section's heading keeps. */
.linkbtn.is-spaced { margin-top: var(--sp-2); }
.linkbtn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; border-radius: var(--r-xs); }

/* ================================================================ field
   One look for every box a person types in or picks from: input.field, select.field,
   textarea.field. The ring on focus is --focus-ring, the same blue haze everywhere.

   A labelled field is a .formfield: the label's words over the control, a hint under
   it (.ff-hint), an error under that (.ff-err, which also turns the control's edge
   red). .genfield is the same thing under the name the admin's panel gave it first,
   and .genfields a row of them that wraps. */
select.field, input.field {
  width: 100%; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-md);
  border: 1px solid var(--line); background: var(--surface-2); font-size: var(--fs-md); font-weight: var(--fw-semi);
}
select.field:focus, input.field:focus { outline: 0; border-color: var(--accent); box-shadow: var(--ring-3) color-mix(in srgb, var(--accent) 20%, transparent); }
/* A text box, matched to the single-line fields beside it. */
textarea.field { width: 100%; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-md);
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  font-family: var(--font); font-size: var(--fs-md); font-weight: var(--fw-medium); line-height: 1.5; }
textarea.field:focus { outline: 0; border-color: var(--accent);
  box-shadow: var(--ring-3) color-mix(in srgb, var(--accent) 20%, transparent); }
textarea.field::placeholder { color: var(--ink-3); }
.field::placeholder { color: var(--ink-3); }
.field:disabled { opacity: .55; cursor: not-allowed; }
.field.is-invalid, .formfield.is-invalid .field { border-color: var(--danger); }
.field.is-invalid:focus, .formfield.is-invalid .field:focus {
  box-shadow: var(--ring-3) color-mix(in srgb, var(--danger) 20%, transparent); }

.formfields, .genfields { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: var(--sp-2); }
.formfield, .genfield { display: grid; gap: var(--sp-1); font-size: var(--fs-xs); font-weight: var(--fw-semi); color: var(--ink-3); letter-spacing: .04em; text-transform: uppercase; }
.formfield input[type="number"], .genfield input[type="number"] { text-transform: none; letter-spacing: 0; }
.check, .genfield-check { display: flex; align-items: center; gap: var(--sp-2); text-transform: none; letter-spacing: 0; font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-2); }
.check input, .genfield-check input { accent-color: var(--accent); width: 15px; height: 15px; }
.ff-hint { font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--ink-3);
  text-transform: none; letter-spacing: 0; line-height: 1.45; }
.ff-err { font-size: var(--fs-xs); font-weight: var(--fw-semi); color: var(--danger);
  text-transform: none; letter-spacing: 0; }
.ff-err:empty { display: none; }
/* A checkbox or a radio with its words: .check. The box is the browser's own, in the
   accent, at the size of a line of small text. */
.check input[type="radio"] { border-radius: 50%; }
.check input:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
.check:has(input:disabled) { opacity: .55; }

/* A switch: on or off, now, with no Save. The input is the browser's, hidden but
   still the thing focused and announced; the track and knob are drawn beside it.
   --switch-on is the colour of on (the accent; the rules page says "in force" in
   green). */
.switch { --switch-on: var(--accent); position: relative; display: inline-flex; align-items: center;
  gap: var(--sp-2); flex: none; cursor: pointer; }
.switch > input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.switch-knob { position: relative; flex: none; width: 44px; height: 24px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink-3) 35%, transparent); transition: background var(--dur-moderate); }
.switch-knob::after { content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px;
  border-radius: 50%; background: var(--surface); box-shadow: var(--shadow-sm);
  transition: transform var(--dur-moderate) var(--ease-out); }
.switch > input:checked + .switch-knob { background: var(--switch-on); }
.switch > input:checked + .switch-knob::after { transform: translateX(20px); }
.switch > input:focus-visible + .switch-knob { outline: var(--focus-outline); outline-offset: 2px; }
.switch > input:disabled + .switch-knob { opacity: .5; cursor: not-allowed; }
.switch-label { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-2); }

/* ================================================================ segmented control
   A row of choices with the chosen one lifted: Day & night | By service, a month's
   range, an email's tone, a page's tabs. .seg is the track, .seg-opt each choice,
   .is-on (or aria-selected) the chosen one. glide.js puts a thumb under the chosen
   option and slides it to the next (.has-thumb, .seg-thumb).

   Variants: .is-pill rounds the track and the options; .is-fill fills the chosen
   option with --seg-on (the accent unless a page says otherwise) rather than lifting
   it; .is-stretch makes the options share the width; .is-scroll lets a long row scroll
   sideways rather than wrap; .is-sm is the compact size; .is-end pushes the control to
   the end of a toolbar it sits in. */
.seg { --seg-on: var(--accent); display: inline-flex; flex-wrap: wrap; gap: var(--sp-half);
  padding: var(--sp-1); border-radius: var(--r-md); background: var(--surface-3); }
.seg-opt { padding: var(--sp-2) var(--sp-3); border: 0; border-radius: var(--r-sm); cursor: pointer;
  background: none; font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-3);
  transition: background var(--dur-base), color var(--dur-base), box-shadow var(--dur-base); }
.seg-opt:hover { color: var(--ink); }
.seg-opt:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
.seg-opt:disabled { cursor: not-allowed; opacity: .5; }
.seg-opt.is-on, .seg-opt[aria-selected="true"] { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-sm); }
.seg.is-end { margin-left: auto; }
@media (max-width: 760px) { .seg.is-end { margin-left: 0; } }
.seg.is-pill, .seg.is-pill .seg-opt, .seg.is-pill .seg-thumb { border-radius: var(--r-pill); }
.seg.is-pill { background: var(--surface-2); border: var(--bw) solid var(--line-soft); }
.seg.is-fill .seg-opt.is-on { background: var(--seg-on); color: var(--on-fill); box-shadow: none; }
.seg.is-stretch { display: flex; }
.seg.is-stretch .seg-opt { flex: 1 0 auto; }
.seg.is-scroll { display: flex; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.seg.is-scroll .seg-opt { white-space: nowrap; }
.seg.is-scroll::-webkit-scrollbar { display: none; }
.seg.is-sm .seg-opt { padding: var(--sp-1) var(--sp-3); }
/* With a thumb under it, the chosen part is the thumb's to draw, and the thumb slides. */
.seg.has-thumb { position: relative; isolation: isolate; }
.seg.has-thumb .seg-opt { position: relative; z-index: var(--z-lift-1); transition: color var(--dur-moderate); }
.seg.has-thumb .seg-opt.is-on { background: transparent; box-shadow: none; }
.seg-thumb { position: absolute; top: 0; left: 0; z-index: var(--z-flat);
  border-radius: var(--r-sm); background: var(--surface); box-shadow: var(--shadow-sm);
  transition: transform var(--dur-slow) var(--ease-out), width var(--dur-slow) var(--ease-out),
    height var(--dur-slow) var(--ease-out); }
.seg.is-fill .seg-thumb { background: var(--seg-on); box-shadow: none; }
.seg.is-fill.has-thumb .seg-opt.is-on { color: var(--on-fill); }
.seg-thumb.is-still { transition: none; }
/* A control that cannot be changed now is faded whole, thumb and all. */
.seg:has(.seg-opt:disabled) .seg-thumb { opacity: .5; }

/* ================================================================ card, tile, stat
   A card is a surface that holds a section of a page. A tile is a block of something
   to read or press, in a colour (--tone); a stat is a tile whose point is a figure. */
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); overflow: hidden; }
.card.is-pad { padding: var(--sp-5); }
/* A tile: a block of something to read or press, on the surface it sits on. */
.tile {
  --tone: var(--accent);
  display: grid; gap: var(--sp-1); padding: var(--sp-4); border-radius: var(--r-lg);
  font: inherit; text-align: left; text-decoration: none; color: var(--ink);
  background:
    radial-gradient(160px 90px at 0% 0%, color-mix(in srgb, var(--tone) 14%, transparent),
      transparent 70%),
    var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--tone) 24%, var(--line-soft));
  transition: transform var(--dur-base), box-shadow var(--dur-base), border-color var(--dur-base);
}
/* With an icon chip to its left, spanning the title and the line under it. */
.tile.has-icon {
  grid-template-columns: auto 1fr; column-gap: var(--sp-3); row-gap: var(--sp-half);
  align-items: center;
}
.tile.has-icon > .ichip { grid-row: span 2; }
/* The chip is a span too, and the grey just below is meant for the line of text: it
   drew every tile's icon in grey on its own colour, a glyph nobody could make out. */
.tile > .ichip { font-size: inherit; align-self: center; }
.tile > b { font-size: var(--fs-base); font-weight: var(--fw-bold); align-self: end; }
.tile > span { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-3);
  align-self: start; }
/* One that goes somewhere lifts toward you, in its own colour. */
a.tile, button.tile { cursor: pointer; }
a.tile:hover, button.tile:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--tone) 45%, transparent);
  box-shadow: var(--glow-md) var(--tone);
}
@media (prefers-reduced-motion: reduce) { a.tile:hover, button.tile:hover { transform: none; } }
/* A stat: a figure, what it counts, and a line under it. */
.stat { --tone: var(--accent); display: grid; gap: var(--sp-half); padding: var(--sp-4);
  border-radius: var(--r-lg); background: var(--surface-2);
  border: var(--bw) solid color-mix(in srgb, var(--tone) 24%, var(--line-soft)); }
.stat > b { font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -.02em;
  color: color-mix(in srgb, var(--tone) 80%, var(--ink)); }
.stat > span { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-2); }
.stat > i { font-style: normal; font-size: var(--fs-xs); color: var(--ink-3); }

/* ================================================================ icon chip, avatar
   Colour comes in as a variable, so a part is one rule and its variants are one line:
   --ic for a chip (and --ic-2 where it is a two-colour gradient), --pc for an avatar,
   which is the colour the person's name wears on the calendar. .avatars is a stack of
   them, each over the last's edge. */
/* An icon chip: a glyph in its colour on a wash of the same colour, the way the rail
   lights the page you are on. It was a white glyph on a dark gradient of its colour
   with a glow under it - a row of them was the loudest thing on every page, and
   nothing like the soft rail beside them. */
.ichip {
  --ic: var(--accent);
  width: 40px; height: 40px; flex: none; border-radius: var(--r-md);
  display: grid; place-items: center; font-style: normal;
  color: color-mix(in srgb, var(--ic) 82%, var(--ink));
  background: color-mix(in srgb, var(--ic) 13%, var(--surface));
  box-shadow: inset var(--ring-1) color-mix(in srgb, var(--ic) 20%, transparent);
}
.ichip svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
.ichip.is-lg { width: 48px; height: 48px; border-radius: var(--r-lg); }
.ichip.is-lg svg { width: 24px; height: 24px; }

/* An avatar: somebody's initials, round, in their colour. Four sizes - the rail's row,
   a card, a panel's head, a page about them. */
/* No default --pc set here: the colour is usually set on the card or hero around the
   avatar and inherited, and a default on the avatar itself would shadow it. */
.avatar {
  width: 32px; height: 32px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  font-size: var(--fs-sm); font-weight: var(--fw-bold); letter-spacing: .02em;
  color: var(--on-fill);
  background: linear-gradient(135deg, var(--pc, var(--accent)),
    color-mix(in srgb, var(--pc, var(--accent)) 55%, var(--shade)));
}
.avatar.is-xs { width: 24px; height: 24px; font-size: var(--fs-2xs); }
.avatar.is-md { width: 48px; height: 48px; font-size: var(--fs-lg); }
.avatar.is-xl { width: 88px; height: 88px; font-size: var(--fs-3xl); }
.avatar.is-sm { width: 32px; height: 32px; }
.avatars { display: inline-flex; align-items: center; }
.avatars > .avatar { box-shadow: var(--ring-2) var(--surface); }
.avatars > .avatar + .avatar { margin-left: -8px; }
.avatars > .avatar.is-more { background: var(--surface-3); color: var(--ink-2); }

/* ================================================================ pill, badge
   A pill is a word or two about the thing it sits beside: its state, its kind. Tones by
   meaning - is-ok, is-wait (the app is waiting on somebody), is-warn, is-danger,
   is-ask (anything to do with a round of requests, in its gold), is-muted - and
   is-outline for one drawn as a ring, is-caps for one set in small capitals. .genbadge
   is the admin panel's name for the small-caps muted pill, kept because the draft
   list and its tests say it.

   A badge is a count: a number in a disc, on a button or a row. */
.pill { display: inline-flex; align-items: center; gap: var(--sp-1); justify-self: start;
  padding: var(--sp-half) var(--sp-2); border-radius: var(--r-pill);
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); white-space: nowrap; line-height: 1.5;
  color: var(--ink-3); background: var(--surface-3); }
.pill.is-caps, .genbadge { letter-spacing: .06em; text-transform: uppercase; }
.genbadge { display: inline-flex; align-items: center; padding: var(--sp-half) var(--sp-2);
  border-radius: var(--r-pill); font-size: var(--fs-3xs); font-weight: var(--fw-bold);
  white-space: nowrap; color: var(--ink-3); background: var(--surface-3); }
.pill.is-xs { font-size: var(--fs-3xs); }
.pill.is-ok { color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, transparent); }
.pill.is-wait { color: var(--accent); background: var(--accent-soft); }
.pill.is-warn, .genbadge.is-off { color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, transparent); }
.pill.is-danger { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }
.pill.is-ask { color: var(--ask-ink); background: var(--ask-grad); }
.pill.is-muted { color: var(--ink-3); background: var(--surface-3); }
.pill.is-outline { background: transparent; border: var(--bw) solid var(--line); }
.pill.is-outline.is-wait { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.pill.is-outline.is-danger { border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
.pill.is-dashed { background: transparent; border: var(--bw) dashed var(--line); }

.badge { min-width: 17px; height: 17px; padding: 0 var(--sp-1); border-radius: var(--r-pill);
  display: inline-grid; place-items: center; font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  font-style: normal; background: var(--accent); color: var(--accent-ink); }
.badge.is-danger { background: var(--danger); color: var(--on-state); }
.badge.is-muted { background: var(--surface-3); color: var(--ink-2); }

/* ================================================================ service chip
   A shift on the calendar: the service's colour (--c) as a tint and a code badge, the
   person's name, and the marks a shift can carry (OT, long or short). The states a
   page puts on it - open, offered, a draft's, the one being dragged - are the page's,
   in its own sheet, compound with .chip. */
.chip {
  --c: var(--ink-3);
  display: flex; align-items: center; gap: var(--sp-1);
  width: 100%; padding: var(--sp-1) var(--sp-2);
  /* A soft tint and a code badge, no stripe down the side. The stripe was the service
     colour said twice and the loudest thing on a page made of hundreds of these. */
  border: 1px solid color-mix(in srgb, var(--c) calc(var(--tint-edge) * .55), transparent);
  border-radius: var(--r-md); cursor: pointer; text-align: left;
  background: color-mix(in srgb, var(--c) calc(var(--tint) * .75), var(--surface));
  box-shadow: var(--shadow-xs);
  color: var(--ink); font-size: var(--fs-sm); font-weight: var(--fw-semi); line-height: 1.35;
  transition: transform var(--dur-fast) var(--ease-pop), box-shadow var(--dur-base), filter var(--dur-base);
}
.chip:hover { transform: translateY(-1px) scale(1.012); box-shadow: var(--shadow-md); z-index: var(--z-lift-2); }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chip .code { font-weight: var(--fw-bold); font-size: var(--fs-2xs); letter-spacing: .04em; flex: none;
  padding: 0 var(--sp-1); border-radius: var(--r-xs); line-height: 1.5;
  color: color-mix(in srgb, var(--c) 82%, var(--ink));
  background: color-mix(in srgb, var(--c) 18%, transparent); }
.chip .who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip .ot { margin-left: auto; flex: none; font-size: var(--fs-3xs); font-weight: var(--fw-bold); letter-spacing: .06em; padding: var(--sp-half) var(--sp-1); border-radius: var(--r-xs); background: color-mix(in srgb, var(--warn) 26%, transparent); color: var(--warn); }
/* Long or short, worn the same way as OT. The long one is the one worth spotting -
   it is the member the ward hands over to at 19:30 - so it carries the colour and
   the short one stays quiet. */
.chip .len { margin-left: auto; flex: 0 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; font-size: var(--fs-3xs); font-weight: var(--fw-bold); letter-spacing: .05em; padding: var(--sp-half) var(--sp-1); border-radius: var(--r-xs); background: color-mix(in srgb, var(--ink-3) 14%, transparent); color: var(--ink-3); }
.chip .len.is-long { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
.chip .ot + .len { margin-left: var(--sp-1); }

.chip.is-night { background: color-mix(in srgb, var(--c) var(--tint), var(--surface-3)); }
.chip.is-canceled { --c: var(--ink-3); border-style: dashed; opacity: .5; }
.chip.is-canceled .who { text-decoration: line-through; }
.chip.is-dim { opacity: .2; filter: saturate(.2); }
.chip.is-hit { box-shadow: var(--ring-2) var(--accent); }
.chip.is-source { box-shadow: var(--ring-2) var(--accent), var(--shadow-md); transform: scale(1.02); }
.chip.is-queued { box-shadow: var(--ring-2) var(--ok); }
.chip.is-target { box-shadow: var(--ring-2) var(--ok); }
.chip[draggable="true"]:active { cursor: grabbing; }
.chip.dragging { opacity: .35; }

/* ================================================================ notice, banner
   A notice is a message inside a page or a dialog: the app talking about the thing
   in front of you. Its tone is its meaning - the accent (info) by default, .warn,
   .err, .is-ok. A banner is the unit talking to everybody: an announcement across the
   top of the page, in one of three tones (the accent, .is-urgent, .is-good). */
.notice { min-width: 0; overflow-wrap: anywhere; padding: var(--sp-3) var(--sp-3); border-radius: var(--r-md); font-size: var(--fs-sm); font-weight: var(--fw-semi); line-height: 1.5; display: grid; gap: var(--sp-1); }
.notice.err { background: color-mix(in srgb, var(--danger) 12%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent); color: color-mix(in srgb, var(--danger) 85%, var(--ink)); }
.notice.warn { background: color-mix(in srgb, var(--warn) 13%, transparent); border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent); color: color-mix(in srgb, var(--warn) 88%, var(--ink)); }
/* The settled counterpart of .warn. A round somebody has already answered is not a
   warning and should not wear the colour of one, but it still needs the band - the
   deadline is the thing they came back for. */
.notice.is-ok { background: color-mix(in srgb, var(--ok) 11%, transparent); border: 1px solid color-mix(in srgb, var(--ok) 32%, transparent); color: color-mix(in srgb, var(--ok) 85%, var(--ink)); }
.notice b { display: block; font-size: var(--fs-2xs); letter-spacing: .08em; text-transform: uppercase; font-weight: var(--fw-bold); opacity: .85; }
.notice:not(.err):not(.warn):not(.is-ok) {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  color: color-mix(in srgb, var(--accent) 80%, var(--ink));
}
.banner { --k: var(--accent); position: relative; overflow: hidden; display: flex;
  align-items: flex-start; gap: var(--sp-4); margin: 0 0 var(--sp-4);
  padding: var(--sp-4) var(--sp-4) var(--sp-4) var(--sp-5); border-radius: var(--r-lg);
  border: 1px solid color-mix(in srgb, var(--k) 34%, var(--line));
  background:
    radial-gradient(520px 160px at 0% 0%, color-mix(in srgb, var(--k) 20%, transparent), transparent 70%),
    radial-gradient(420px 140px at 100% 100%, color-mix(in srgb, var(--hue-indigo) 10%, transparent), transparent 70%),
    color-mix(in srgb, var(--k) 5%, var(--surface));
  box-shadow: var(--glow-md) color-mix(in srgb, var(--k) 55%, transparent), var(--shadow-sm);
  animation: banner-in var(--dur-slower) var(--ease-out) both; }
.banner.is-urgent { --k: var(--danger); }
.banner.is-good { --k: var(--ok); }
/* A slow sheen across it, once, as it arrives: enough to catch the eye on a page that
   otherwise does not move, not enough to keep catching it. */
.banner::after { content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 35%, color-mix(in srgb, var(--on-fill) 28%, transparent) 50%, transparent 65%);
  transform: translateX(-100%); animation: banner-sheen var(--loop-2) var(--dur-slower) ease-out both; }
.banner::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px;
  background: linear-gradient(180deg, var(--k), color-mix(in srgb, var(--k) 40%, var(--hue-indigo))); }
.banner.is-leaving { animation: banner-out var(--dur-moderate) ease-in both; }

.bn-icon { position: relative; flex: none; display: grid; place-items: center; width: 40px;
  height: 40px; border-radius: var(--r-md); color: var(--on-fill);
  background: linear-gradient(135deg, var(--k), color-mix(in srgb, var(--k) 65%, var(--hue-indigo)));
  box-shadow: var(--glow-sm) var(--k); }
.bn-icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
.banner.is-urgent .bn-icon::after { content: ""; position: absolute; inset: -4px;
  border-radius: var(--r-lg); border: 2px solid var(--k); animation: banner-ring var(--loop-2) ease-out infinite; }
.bn-text { flex: 1; min-width: 0; display: grid; gap: var(--sp-half); }
.bn-kick { font-size: var(--fs-2xs); font-weight: var(--fw-bold); letter-spacing: .06em;
  text-transform: uppercase; color: color-mix(in srgb, var(--k) 75%, var(--ink)); }
.bn-title { font-size: var(--fs-lg); font-weight: var(--fw-bold); letter-spacing: -.01em;
  color: var(--ink); }
.bn-body { margin: var(--sp-1) 0 0; font-size: var(--fs-md); line-height: 1.5; color: var(--ink-2);
  white-space: pre-line; }
.bn-side { flex: none; display: flex; align-items: center; gap: var(--sp-2); }
.bn-step { display: flex; align-items: center; gap: var(--sp-1); }
.bn-count { font-size: var(--fs-xs); font-weight: var(--fw-semi); color: var(--ink-3);
  min-width: 44px; text-align: center; }
.bn-btn { display: grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 0;
  border-radius: var(--r-sm); background: transparent; color: var(--ink-3); cursor: pointer;
  transition: background var(--dur-base) ease, color var(--dur-base) ease; }
.bn-btn:hover { background: color-mix(in srgb, var(--k) 12%, transparent); color: var(--ink); }
.bn-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round; }
@keyframes banner-in { from { opacity: 0; transform: translateY(-8px) scale(.99); } }
@keyframes banner-out { to { opacity: 0; transform: translateY(-6px); } }
@keyframes banner-sheen { to { transform: translateX(100%); } }
@keyframes banner-ring { from { opacity: .7; transform: scale(.9); } to { opacity: 0; transform: scale(1.25); } }
@media (prefers-reduced-motion: reduce) {
  .banner, .banner::after, .banner.is-urgent .bn-icon::after { animation: none; }
  .banner::after { display: none; }
}
@media (max-width: 640px) {
  .banner { gap: var(--sp-3); padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4); }
  .bn-icon { width: 32px; height: 32px; }
  .bn-side { flex-direction: column-reverse; align-items: flex-end; }
  .bn-count { display: none; }
}

/* ================================================================ empty state
   Nothing here yet: a glyph, a line in large type, and a sentence saying what would
   put something here. .is-small for one inside a card rather than in place of a page. */
.emptyboard {
  display: grid; justify-items: center; align-content: center; gap: var(--sp-2);
  min-height: 300px; padding: var(--sp-10) var(--sp-6); text-align: center;
  border-radius: var(--r-lg); border: 1.5px dashed var(--line);
  background:
    radial-gradient(420px 220px at 50% 0%,
      color-mix(in srgb, var(--accent) 8%, transparent), transparent 70%),
    var(--surface-2);
}
.emptyboard svg { width: 46px; height: 46px; fill: none; stroke: var(--ink-3);
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; opacity: .55;
  margin-bottom: var(--sp-1); }
.emptyboard b { font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -.01em;
  color: var(--ink-3); }
.emptyboard span { max-width: 44ch; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  line-height: 1.55; color: var(--ink-3); }
.emptyboard.is-small { min-height: 150px; padding: var(--sp-6) var(--sp-5); }
.emptyboard.is-small b { font-size: var(--fs-xl); }
@media (max-width: 760px) { .emptyboard { min-height: 220px; } }

/* ================================================================ table
   A table of figures: small-caps headings, a hairline between rows, the row under the
   pointer lifted. .gentable is the name the admin's panel gave it, and the one the
   pages use. */
.gentable { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-sm); }
.gentable th {
  text-align: left; padding: var(--sp-2) var(--sp-2); font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3);
  border-bottom: 1px solid var(--line);
}
.gentable td { padding: var(--sp-1) var(--sp-2); border-bottom: 1px solid var(--line-soft); }
.gentable tbody tr:hover td { background: var(--surface-2); }
.gentable input[type="checkbox"] { accent-color: var(--accent); width: 15px; height: 15px; }

/* ================================================================ list row
   One thing in a list, as a row: something to say who or what (an avatar, an icon
   chip), the words, and what can be done with it at the end. A list of them is .list,
   with a hairline between. */
.list { display: grid; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }
.listrow { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-lg); background: var(--surface); border: var(--bw) solid var(--line-soft);
  transition: border-color var(--dur-base), box-shadow var(--dur-base); }
.listrow > .lr-body { flex: 1; min-width: 0; display: grid; gap: var(--sp-half); }
.listrow > .lr-body > b { font-size: var(--fs-base); font-weight: var(--fw-bold); }
.listrow > .lr-body > span { font-size: var(--fs-sm); color: var(--ink-3); }
.listrow > .lr-end { flex: none; display: flex; align-items: center; gap: var(--sp-2); }
a.listrow, button.listrow { font: inherit; color: inherit; text-align: left; text-decoration: none; cursor: pointer; }
a.listrow:hover, button.listrow:hover { border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); box-shadow: var(--shadow-sm); }
.listrow:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
.listrow.is-on { border-color: var(--accent); box-shadow: var(--ring-1) var(--accent); }

/* ================================================================ progress, spinner, skeleton
   How far along something is (.progress, the fill an <i> inside it, sized by --p as a
   fraction or by a width; --bar is its colour), that something is happening
   (.spinner, inline at the size of the text, .is-lg for a page's own), and where
   something will be once it has loaded (.skeleton). */
.progress { --bar: var(--accent); height: 6px; border-radius: var(--r-pill); overflow: hidden;
  background: var(--surface-3); }
.progress > i { display: block; height: 100%; width: calc(var(--p, 0) * 100%); border-radius: inherit;
  background: var(--bar); transition: width var(--dur-slow) var(--ease-out); }
.progress.is-lg { height: 8px; }

.spinner { display: inline-block; width: var(--icon-xs); height: var(--icon-xs); border-radius: 50%; flex: none;
  border: var(--bw-2) solid color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor; animation: spin var(--dur-linger) linear infinite; }
.spinner.is-lg { width: 42px; height: 42px; border-width: 3px; border-color: var(--line);
  border-top-color: var(--accent); }
@keyframes spin { to { transform: rotate(360deg); } }

.skeleton { display: block; min-height: 1em; border-radius: var(--r-md);
  background: linear-gradient(100deg, color-mix(in srgb, var(--ink) 5%, transparent) 30%,
    color-mix(in srgb, var(--ink) 11%, transparent) 50%, color-mix(in srgb, var(--ink) 5%, transparent) 70%);
  background-size: 300% 100%; animation: shimmer var(--loop-2) linear infinite; }
@keyframes shimmer { from { background-position: 100% 0; } to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .spinner, .skeleton, .btn.is-loading::after { animation-duration: var(--loop-4); } }

/* ================================================================ menu
   A short list of actions that opens from a button: .menu, its items buttons inside
   it (.is-danger for the one that cannot be undone). Where it opens is the page's to
   say - the rail's opens upward, over the button that opened it. */
.menu { display: grid; gap: var(--sp-half); padding: var(--sp-1); border-radius: var(--r-lg);
  background: var(--surface); border: var(--bw) solid var(--line); box-shadow: var(--shadow-md);
  animation: menu-in var(--dur-base) var(--ease-out); }
@keyframes menu-in { from { opacity: 0; transform: translateY(6px); } }
.menu > button, .menu > a { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
  border: 0; border-radius: var(--r-md); font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-semi);
  color: var(--ink-2); background: transparent; cursor: pointer; text-align: left; text-decoration: none; }
.menu > button:hover, .menu > button:focus-visible, .menu > a:hover, .menu > a:focus-visible {
  background: var(--surface-2); color: var(--ink); outline: none; }
.menu > .is-danger { color: var(--danger); }
.menu > .is-danger:hover { background: color-mix(in srgb, var(--danger) 8%, transparent); }
@media (prefers-reduced-motion: reduce) { .menu { animation: none; } }

/* ================================================================ dialog
   A dialog over a dimmed page: .modal-scrim behind, .modal in front, .modal-body
   inside it and .modal-actions at its foot (or, .is-top, held at its head). Three
   widths: the default, -wide and -narrow. */
.modal-scrim { position: fixed; inset: 0; z-index: var(--z-scrim); background: var(--scrim); backdrop-filter: blur(3px); animation: fade var(--dur-base); }
@keyframes fade { from { opacity: 0; } }
.modal {
  position: fixed; z-index: var(--z-modal); top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(520px, calc(100vw - 32px)); max-height: min(86vh, 780px); overflow-y: auto;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl); box-shadow: var(--shadow-lg);
  animation: pop var(--dur-moderate) var(--ease-out);
}
.modal-wide { width: min(680px, calc(100vw - 32px)); }
.modal-narrow { width: min(400px, calc(100vw - 32px)); }
@keyframes pop { from { opacity: 0; transform: translate(-50%, -46%) scale(.97); } }
.modal-body { padding: var(--sp-6) var(--sp-6) var(--sp-6); display: grid; gap: var(--sp-4); }
.modal h2 { font-size: var(--fs-xl); letter-spacing: -.025em; }
.modal .sub { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--ink-3); font-weight: var(--fw-semi); line-height: 1.5; }
/* Wraps, because the labels in here are sentences rather than words - the swap
   list's button names the number of options it will draw - and three of them on a
   narrow screen would otherwise run off the end of a row that cannot scroll. */
.modal-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); justify-content: flex-end;
  padding-top: var(--sp-1); }
/* For a modal long enough to scroll, where the actions are the point and the length is
   the content. Spans the body's padding so nothing scrolls through the gutters. */
.modal-actions.is-stuck { position: sticky; bottom: 0; z-index: var(--z-lift-1); margin: 0 -24px -24px; padding: var(--sp-3) var(--sp-6) var(--sp-4); background: var(--surface); border-top: 1px solid var(--line-soft); }
.modal-close { position: absolute; top: 16px; right: 16px; }
/* The same actions, held at the top of a long panel instead of the foot: the thing to
   do with a list is read it on the calendar, and that should not wait for a scroll. */
.modal-actions.is-top {
  /* Above a shift lit up in the panel, which is lifted to stand out from the calendar. */
  position: sticky; top: 0; z-index: var(--z-lift-5); justify-content: flex-start;
  margin: 0 calc(-1 * var(--sp-6)); padding: var(--sp-3) var(--sp-6);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line-soft);
}
.modal-actions.is-top .btn-primary { flex: 1 1 100%; justify-content: center; }
/* A dialog on a phone is the screen, not a box floating in the middle of it. */
@media (max-width: 760px) {
  .modal, .modal-wide, .modal-narrow {
    width: 100%; max-width: 100%; max-height: 100vh; border-radius: 0;
    top: 0; left: 0; transform: none; bottom: 0;
  }
}

/* ================================================================ toast
   A line that comes up from the foot of the screen to say a thing happened, and goes.
   .err and .ok for the two that are news; a button inside for Undo. */
.toasts { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: var(--z-toast); display: grid; gap: var(--sp-2); justify-items: center; pointer-events: none; }
.toast {
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); font-size: var(--fs-sm); font-weight: var(--fw-semi);
  background: var(--ink); color: var(--bg); box-shadow: var(--shadow-lg);
  display: flex; align-items: center; gap: var(--sp-2); pointer-events: auto;
  animation: toastin var(--dur-slow) var(--ease-out);
}
.toast.err { background: var(--danger); color: var(--on-state); }
.toast.ok { background: var(--ok); color: var(--on-state); }
.toast button { border: 0; background: color-mix(in srgb, var(--on-fill) 22%, transparent); color: inherit; border-radius: var(--r-sm); padding: var(--sp-1) var(--sp-2); font-size: var(--fs-sm); font-weight: var(--fw-bold); cursor: pointer; }
@keyframes toastin { from { opacity: 0; transform: translateY(12px); } }

/* ================================================================ headers
   A page's heading: a kicker in the accent, the title, a line under it saying what the
   page is for (.pagehead, its parts .pk and .ps). A section's: a small-caps label
   (.subhead). */
.pagehead { display: grid; gap: var(--sp-1); padding: var(--sp-2) var(--sp-1) 0; }
.pagehead h2 { margin: 0; font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -.02em; }
/* The kicker and the line under the title. Classes of their own rather than parts
   only a .pagehead can hold, because a page's sub-line - "Round 3 · Mar–Apr · open"
   - is the same line in the same voice wherever it is. */
.pk { margin: 0; font-size: var(--fs-2xs); font-weight: var(--fw-bold); letter-spacing: .12em;
  text-transform: uppercase; color: var(--accent); }
.ps { margin: 0; max-width: 64ch; font-size: var(--fs-md); font-weight: var(--fw-medium);
  color: var(--ink-3); line-height: 1.5; }
/* A section's heading: a small-caps label. */
.subhead { font-size: var(--fs-2xs); font-weight: var(--fw-bold); letter-spacing: .11em; text-transform: uppercase; color: var(--ink-3); }
.subhead.is-spaced { margin-bottom: var(--sp-2); }

/* ================================================================ door
   The pages before the gate - the gate itself, an invitation, a swap approval opened
   from an email, a read-only link that will not open - are one card in the middle of
   the page. Each used to draw its own: four cards, three primary buttons (one flat,
   two in a gradient), three sets of labels and boxes and a status disc called .badge,
   which here is a count. They load this sheet beside the tokens (both under
   /assets/ds/, the part of /assets that is open to the door) and are drawn from it.

   .door-page is the body behind the card: the page's colour with the door's two washes
   in its corners (.is-page where the body is a page rather than one card). .door is
   the card, the door's gradient as a strip along its top; .is-wide for one with a list
   of shifts in it, .is-narrow for the gate's one field. Inside it: .door-logo, .door-head (a centred kicker, title and
   lead), .door-kicker, h1, .door-lead, .door-form (labels that are .formfield, boxes
   that are .field, buttons that are .btn), .door-err (what went wrong, in the card
   rather than a toast), .door-foot, and .door-status - a state in a disc beside the
   title: .is-ok, .is-no, .is-warn, .is-wait (turning). */
.door-page {
  margin: 0; min-height: 100vh; min-height: 100dvh; display: grid; place-items: center;
  padding: var(--sp-6) var(--sp-4); font-family: var(--font); color: var(--ink);
  -webkit-font-smoothing: antialiased;
  background:
    radial-gradient(1100px 600px at 15% -10%, color-mix(in srgb, var(--door-a) 12%, transparent), transparent 65%),
    radial-gradient(900px 500px at 100% 0%, color-mix(in srgb, var(--door-b) 16%, transparent), transparent 60%),
    var(--bg);
}
/* A page with more on it than the card - a read-only link's rota - wears the door's
   background and base but is laid out as a page, from the top. */
.door-page.is-page { display: block; padding: 0; }
/* The element base the app has in base.css, which the door does not load: controls
   in the page's type. At no specificity, so .btn and .field still say their own size. */
:where(.door-page) :where(button, input, select, textarea) { font: inherit; color: inherit; }
:where(.door-page) *, :where(.door-page) *::before, :where(.door-page) *::after { box-sizing: border-box; }
:where(.door-page) svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round; }
.door {
  position: relative; width: min(440px, 100%); display: grid; gap: var(--sp-5);
  padding: var(--sp-8) var(--sp-8) var(--sp-6); border-radius: var(--r-xl);
  border: var(--bw) solid var(--line); background: var(--surface); box-shadow: var(--shadow-lg);
  overflow: hidden; animation: door-in var(--dur-slower) var(--ease-out);
}
.door::before { content: ""; position: absolute; inset: 0 0 auto; height: 4px;
  background: linear-gradient(90deg, var(--door-a), var(--door-b)); }
.door.is-wide { width: min(480px, 100%); gap: var(--sp-4); }
.door.is-narrow { width: min(380px, 100%); }
@keyframes door-in { from { opacity: 0; transform: translateY(10px); } }
.door h1 { margin: 0; font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -.02em; }
.door-logo { display: block; justify-self: center; width: 100%; max-width: 220px; height: auto;
  mix-blend-mode: var(--logo-blend); filter: var(--logo-filter); }
.door-head { display: grid; gap: var(--sp-1); text-align: center; }
.door-kicker { margin: 0; font-size: var(--fs-xs); font-weight: var(--fw-bold); letter-spacing: .12em;
  text-transform: uppercase; color: var(--door-text); }
.door-lead { margin: 0; font-size: var(--fs-md); font-weight: var(--fw-medium); color: var(--ink-3);
  line-height: 1.55; }
.door-form { display: grid; gap: var(--sp-3); }
/* 16px in a box that is typed in: iOS zooms the page into anything smaller on focus. */
.door .field { padding: var(--sp-3) var(--sp-4); font-size: var(--fs-lg); }
.door .btn { justify-content: center; text-align: center; text-decoration: none; }
.door-err { margin: 0; font-size: var(--fs-sm); font-weight: var(--fw-semi); text-align: center;
  color: var(--danger); }
.door-err:empty { display: none; }
.door-foot { margin: 0; font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--ink-3);
  text-align: center; }
.door-status { flex: none; width: 48px; height: 48px; border-radius: 50%; display: grid;
  place-items: center; color: var(--on-state); background: var(--line); }
.door-status svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round; }
.door-status.is-ok { background: var(--ok); }
.door-status.is-no { background: var(--danger); }
.door-status.is-warn { background: var(--warn); }
.door-status.is-wait { color: var(--ink-3); }
.door-status.is-wait svg { animation: spin var(--dur-linger) linear infinite; }
@media (max-width: 480px) { .door { padding: var(--sp-6) var(--sp-5) var(--sp-5); } }
@media (prefers-reduced-motion: reduce) { .door, .door-status.is-wait svg { animation: none; } }
