/* ============================================================================
   base.css — STRUCTURE ONLY.

   Never a colour, radius, shadow, spacing or font literal in here. Everything
   visual comes from the token contract that a style (styles/*.css) and a
   palette (palettes/*.css) fill in between them:

     style   — shape, size, typography, component behaviour  [data-style="…"]
     palette — colour, both modes                            [data-palette="…"]

   Both are linked from base.html and selected by JANUS_UI_STYLE /
   JANUS_UI_PALETTE. Because the two files are attribute-scoped they never
   collide, which is what makes them swappable at runtime.

   tests/test_theme.py fails the build if a style leaks a colour, a palette
   leaks a geometry token, or any combination leaves a token undefined.
   ========================================================================= */

/* Self-hosted so the display face survives offline — the service worker caches
   /static/*. Exo 2 is a variable font, so one file covers the 600-700 range.
   Defining the face is not the same as choosing it: a style still picks via
   --font-display. Exo 2 is licensed under the SIL Open Font License 1.1. */
@font-face {
  font-family: "Exo 2";
  font-style: normal;
  font-weight: 600 700;
  font-display: swap;
  src: url("/static/fonts/exo2-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Exo 2";
  font-style: normal;
  font-weight: 600 700;
  font-display: swap;
  src: url("/static/fonts/exo2-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.5;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
.wordmark {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  letter-spacing: var(--tracking-display);
  margin: 0 0 0.25em;
}

h1 {
  font-size: var(--text-h1);
}

p {
  margin: 0 0 0.75em;
}

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

/* Inline icons sit next to text, so nudge them onto the optical baseline. */
.icon {
  vertical-align: -0.15em;
  flex: 0 0 auto;
}

.mono {
  font-family: var(--font-mono);
  font-size: 0.88em;
  letter-spacing: -0.01em;
}

/* ---------------------------------------------------------------- layout */

/* Horizontal insets keep content clear of rounded corners / notch in landscape;
   env() is 0 on phones without a cutout, so the gutter still applies. */
.container {
  max-width: 1120px;
  margin: 0 auto;
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
}

/* Vertical longhands only. `padding: X 0 Y` here would reset the horizontal
   padding .container just set — both are one class and this rule is later, so
   the gutter would silently vanish above the mobile breakpoint. That is a real
   bug the previous stylesheet shipped. */
.page {
  padding-top: var(--space-5);
  padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom));
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.stack-tight {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

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

.filters-row {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: flex-end;
  margin-bottom: var(--space-4);
}

.filters-row .field {
  margin-bottom: 0;
  min-width: 140px;
}

.text-secondary,
.text-2 {
  color: var(--text-2);
}

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

.page-intro {
  color: var(--text-2);
  font-size: var(--text-sm);
  margin: 0;
}

.mt-0 {
  margin-top: 0;
}

/* ---------------------------------------------- top bar + left drawer ---- */

/* The nav paints into the status-bar strip (apple-mobile-web-app-status-bar-style
   is black-translucent), so it carries the top inset itself. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--bg-elev);
  border-bottom: var(--border-w) solid var(--border);
  padding-top: env(safe-area-inset-top);
}

.topbar-inner {
  max-width: 1120px;
  margin: 0 auto;
  padding-left: max(8px, env(safe-area-inset-left));
  padding-right: max(8px, env(safe-area-inset-right));
  display: flex;
  align-items: center;
  gap: var(--space-1);
  height: var(--topbar-h);
}

.topbar .brand {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--text-lg);
  letter-spacing: var(--tracking-display);
  color: var(--text);
  text-decoration: none;
  padding: 0 8px;
}

.topbar .spacer {
  flex: 1;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--radius-sm);
  background: none;
  border: none;
  padding: 0;
  color: var(--text-2);
  text-decoration: none;
  cursor: pointer;
}

.icon-btn:hover {
  color: var(--text);
  background: var(--wash);
}

.icon-btn-bell {
  position: relative;
}

.badge-dot {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  border: 1.5px solid var(--bg-elev);
}

.drawer-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: rgb(0 0 0 / 0.5);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.drawer-backdrop.show {
  opacity: 1;
}

.drawer-backdrop[hidden] {
  display: none;
}

.drawer {
  position: fixed;
  z-index: 60;
  top: 0;
  bottom: 0;
  left: 0;
  width: min(19rem, 82vw);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-right: var(--border-w) solid var(--border);
  transform: translateX(-100%);
  transition: transform 0.22s ease;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  overflow-y: auto;
}

.drawer.open {
  transform: translateX(0);
}

.drawer-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 20px 20px 16px;
  border-bottom: var(--border-w) solid var(--border);
}

.drawer-who {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--text-lg);
  color: var(--text);
}

.drawer-role {
  font-size: var(--text-eyebrow);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}

.drawer-list {
  list-style: none;
  margin: 0;
  padding: 8px 0;
  flex: 1;
}

.drawer-foot {
  padding: 8px 0;
  border-top: var(--border-w) solid var(--border);
}

/* Full-width rows: labels are always visible and the target is 48px, which is
   what the old icon-only top pills could not give at any screen size. */
.drawer-item {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 48px;
  padding: 0 20px;
  color: var(--text-2);
  text-decoration: none;
  font-size: var(--text-sm);
}

.drawer-item-btn {
  width: 100%;
  background: none;
  border: none;
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  text-align: left;
}

.drawer-item:hover {
  background: var(--wash);
  color: var(--text);
}

.drawer-item.active {
  color: var(--text);
  background: var(--wash);
  box-shadow: inset 3px 0 0 var(--accent);
}

.drawer-count {
  margin-left: auto;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--text-eyebrow);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ------------------------------------------------------------- banners --- */

.killswitch-banner {
  background: var(--err);
  color: var(--err-ink);
  text-align: center;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: calc(6px + env(safe-area-inset-top)) 12px 6px;
}

/* Trip mode is easy to forget you left on, so it stays visible everywhere —
   otherwise the first symptom is a guest's link mysteriously not working. */
.trip-banner {
  background: var(--warn);
  color: var(--warn-ink);
  text-align: center;
  font-size: var(--text-xs);
  font-weight: 600;
  padding: calc(6px + env(safe-area-inset-top)) 12px 6px;
}

.killswitch-banner + .topbar,
.trip-banner + .topbar {
  padding-top: 0;
}

.flash {
  border-radius: var(--radius-md);
  border: var(--border-w) solid var(--border);
  border-left: 3px solid var(--info);
  background: var(--surface);
  color: var(--text);
  padding: 10px 14px;
  font-size: var(--text-sm);
  margin-bottom: 12px;
}

.flash-ok {
  border-left-color: var(--ok);
}

.flash-err {
  border-left-color: var(--err);
}

.flash-warn {
  border-left-color: var(--warn);
}

/* --------------------------------------------------------------- cards --- */

.card {
  background: var(--surface);
  border: var(--border-w) solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--card-pad);
  box-shadow: var(--shadow-1);
}

.card-tight {
  padding: 12px 16px;
}

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

.card-title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--text-md);
  letter-spacing: var(--tracking-display);
  margin: 0 0 var(--space-3);
}

.section-head {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: var(--space-3);
  color: var(--text-3);
  font-size: var(--text-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.empty-state {
  text-align: center;
  color: var(--text-3);
  padding: var(--space-5) var(--space-4);
  font-size: var(--text-sm);
}

/* -------------------------------------------------------------- labels --- */

.field-label,
.section-label,
.detail-label {
  display: block;
  font-size: var(--text-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-3);
  margin-bottom: 6px;
}

.detail-label {
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

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

label {
  display: block;
}

.field {
  margin-bottom: var(--space-3);
}

.field-hint {
  font-size: var(--text-xs);
  color: var(--text-3);
  margin: 6px 0 0;
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="datetime-local"],
input[type="time"],
input[type="email"],
select,
textarea {
  width: 100%;
  font: inherit;
  font-size: var(--text-sm);
  min-height: var(--control-h);
  color: var(--text);
  background: var(--input-bg);
  border: var(--border-w) solid var(--input-border);
  border-radius: var(--radius-md);
  padding: 0 var(--space-3);
}

textarea {
  padding: var(--space-3);
}

select {
  appearance: none;
  padding-right: 2.4rem;
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: right 0.8rem center;
  background-size: 1rem;
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 var(--focus-ring-w) var(--accent-wash);
}

/* Every custom control previously had no focus state at all — keyboard users
   had nothing to follow. */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

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

input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
}

.checkbox-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.checkbox-row label {
  font-size: var(--text-sm);
  color: var(--text);
}

/* -------------------------------------------------------------- buttons -- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--control-h);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--btn-text);
  background: var(--btn-bg);
  border: var(--border-w) solid var(--btn-border);
  border-radius: var(--radius-md);
  padding: 0 var(--space-4);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color 0.15s,
    border-color 0.15s,
    transform 0.06s;
}

.btn:hover {
  background: var(--btn-bg-hover);
}

.btn:active {
  transform: scale(0.985);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-accent {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent);
}

.btn-accent:hover {
  background: var(--accent-hover);
}

.btn-secondary,
.btn-quiet {
  background: transparent;
  color: var(--text);
  border-color: var(--border);
}

.btn-secondary:hover,
.btn-quiet:hover {
  background: var(--wash);
}

.btn-danger {
  background: transparent;
  color: var(--err-text);
  border-color: var(--err-text);
}

.btn-danger:hover {
  background: var(--err-wash);
}

.btn-block {
  width: 100%;
}

.btn-sm {
  min-height: 36px;
  font-size: var(--text-xs);
  padding: 0 var(--space-3);
}

.btn-lg {
  min-height: 52px;
  font-size: var(--text-md);
}

.link-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* ------------------------------------------------------- door controls --- */

/* Doors are not wrapped in a .card. Each plate already has its own border and
   elevation, so a card around them is card-inside-a-card nesting — and on a
   phone it spends 2x --card-pad of the one thing in short supply, the width of
   the door's own name. */
.door-section {
  margin-bottom: var(--space-2);
}

.door-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* An empty section still has to be a drop target, so it keeps a height and
   says what it is for. :empty relies on the template emitting no whitespace
   inside the list element. */
.door-list[data-reorder]:empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 68px;
  border: var(--border-w) dashed var(--border);
  border-radius: var(--radius-md);
}

.door-list[data-reorder]:empty::after {
  content: attr(data-empty-hint);
  color: var(--text-3);
  font-size: var(--text-xs);
  text-align: center;
  padding: 0 12px;
}

.door-list.drop-target {
  outline: 2px dashed var(--accent-border);
  outline-offset: 4px;
  border-radius: var(--radius-md);
}

.door-item {
  position: relative;
  display: flex;
  align-items: stretch;
}

/* The dragged row follows the pointer, so it has to sit above its siblings and
   must not animate its own transform while being dragged. */
.door-item.dragging {
  z-index: 5;
  cursor: grabbing;
}

.door-item.dragging .door-btn {
  border-color: var(--accent-border);
  opacity: 0.97;
}

/* Both secondary controls overlay the right end of the plate rather than
   taking columns of their own. Neither can be a *child* of .door-btn — that is
   a <button>, and a button may not contain another one — so they are siblings
   stacked above it. That is also what keeps the plate's swipe handler from
   ever seeing their pointerdown, so grip-drag and swipe-to-open cannot collide.

   The grip used to sit in its own 44px column to the left of every row. Four
   of those down the left edge bought one rare action and broke the left margin
   of the entire list. The star sits outermost because it is tapped far more
   often than the grip is dragged. */
.drag-handle,
.fav-btn {
  position: absolute;
  z-index: 2;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--side-ctl-w);
  background: none;
  border: none;
  padding: 0;
  border-radius: var(--radius-sm);
  color: var(--text-3);
  cursor: pointer;
  font-family: inherit;
}

.fav-btn {
  right: 0;
}

.fav-btn:hover {
  color: var(--text);
}

.fav-btn.is-fav {
  color: var(--accent-text);
}

.fav-btn.is-fav .icon {
  fill: currentColor;
}

.fav-btn:disabled {
  opacity: 0.5;
  pointer-events: none;
}

.drag-handle {
  right: var(--side-ctl-w);
  cursor: grab;
  /* claim the vertical drag from the page scroller */
  touch-action: none;
  user-select: none;
}

.drag-handle:hover {
  color: var(--text-2);
}

.drag-handle:active {
  cursor: grabbing;
  color: var(--text);
}

.door-btn {
  position: relative;
  overflow: hidden;
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--door-h);
  padding-left: calc(var(--thumb-size) + 16px);
  /* clears both overlaid controls: the grip and the star */
  padding-right: calc(2 * var(--side-ctl-w) + var(--space-2));
  background: var(--door-bg);
  color: var(--text);
  border: var(--border-w) solid var(--door-border);
  border-radius: var(--radius-door);
  box-shadow: var(--shadow-1);
  font: inherit;
  font-weight: 650;
  font-size: var(--text-md);
  text-align: left;
  cursor: pointer;
  user-select: none;
}

/* ---------- swipe to open ----------
   A single tap used to fire a real gate. The swipe makes opening deliberate,
   which is the one interaction in this app that must never happen by accident
   in a pocket. Applied by JS (.swipe-ready), so without JS the control stays a
   plain button; keyboard activation also opens directly, since a focused Enter
   press is already deliberate. */

.door-btn.swipe-ready {
  /* let the page scroll vertically, but claim horizontal drags */
  touch-action: pan-y;
}

.door-btn:hover {
  background: var(--btn-bg-hover);
}

.door-btn:active {
  transform: scale(0.99);
}

.door-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.door-btn .meta {
  display: block;
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--text-3);
  margin-top: 2px;
}

.swipe-hint {
  display: block;
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--text-3);
}

.door-schedule {
  display: block;
  font-size: var(--text-xs);
  color: var(--text-3);
  margin-top: 2px;
}

.swipe-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 0;
  background: var(--accent);
  opacity: 0.16;
  pointer-events: none;
}

.swipe-thumb {
  position: absolute;
  top: 50%;
  left: var(--thumb-inset);
  width: var(--thumb-size);
  height: var(--thumb-size);
  margin-top: calc(var(--thumb-size) / -2);
  border-radius: var(--radius-thumb);
  background: var(--accent);
  color: var(--accent-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  box-shadow: var(--shadow-thumb);
}

.door-btn.swiping .swipe-thumb,
.door-btn.swiping .swipe-fill {
  transition: none;
}

.door-btn:not(.swiping) .swipe-thumb,
.door-btn:not(.swiping) .swipe-fill {
  transition:
    transform 0.18s ease,
    width 0.18s ease;
}

.door-btn:disabled .swipe-thumb {
  background: var(--switch-off);
  color: var(--text-3);
}

.status-dot {
  width: 9px;
  height: 9px;
  border-radius: var(--radius-pill);
  background: var(--border);
  flex: 0 0 auto;
}

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

.status-dot.err {
  background: var(--err);
}

/* Open-request states: the thumb stays parked at the end of the track and the
   hint line narrates (Abrindo… / Aberta ✓ / error text) instead of a toast. */

.door-btn.is-opening .swipe-fill {
  animation: swipe-pulse 1.2s ease-in-out infinite;
}

@keyframes swipe-pulse {
  0%,
  100% {
    opacity: 0.16;
  }
  50% {
    opacity: 0.3;
  }
}

.door-btn.is-success {
  border-color: var(--ok-border);
}

.door-btn.is-success .swipe-fill {
  background: var(--ok);
  opacity: 0.24;
}

.door-btn.is-success .swipe-hint {
  color: var(--ok-text);
}

.door-btn.is-error {
  border-color: var(--err-border);
}

.door-btn.is-error .swipe-fill {
  background: var(--err);
  opacity: 0.18;
}

.door-btn.is-error .swipe-thumb {
  background: var(--err);
}

.door-btn.is-error .swipe-hint {
  color: var(--err-text);
  font-weight: 600;
}

/* -------------------------------------------------------- record lists ---

   Replaces the desktop <table> below the breakpoint. A record is one object,
   not seven label/value lines: the previous restack turned a single event into
   most of a phone screen.                                                    */

.records {
  display: flex;
  flex-direction: column;
  gap: var(--rec-gap);
}

.rec {
  background: var(--rec-bg);
  border: var(--border-w) solid var(--rec-border);
  border-radius: var(--radius-md);
  padding: var(--rec-pad);
}

.rec-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.rec-title {
  font-weight: 650;
  font-size: var(--text-sm);
  min-width: 0;
  overflow-wrap: anywhere;
}

.rec-sub {
  display: block;
  font-weight: 400;
  font-size: var(--text-xs);
  color: var(--text-3);
  margin-top: 2px;
}

.rec-right {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}

/* Expandable variant: the head becomes the disclosure control, so a closed row
   stays at two lines and the rest lives in the drawer. */
.rec-expander {
  width: 100%;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  min-height: var(--control-h);
}

.rec-chevron {
  color: var(--text-3);
  transition: transform 0.18s ease;
}

.rec.open .rec-chevron {
  transform: rotate(180deg);
}

.rec-drawer {
  display: none;
}

.rec.open .rec-drawer {
  display: block;
}

.rec-fields {
  display: grid;
  grid-template-columns: minmax(5rem, auto) 1fr;
  gap: 2px var(--space-3);
  margin-top: var(--space-3);
  font-size: var(--text-sm);
}

.rec-fields dt {
  color: var(--text-3);
  font-size: var(--text-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  align-self: baseline;
  padding-top: 3px;
}

.rec-fields dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.rec-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  flex-wrap: wrap;
}

.rec-actions .btn {
  flex: 1;
}

.rec-actions form {
  flex: 1;
  display: flex;
}

.rec-actions form .btn {
  width: 100%;
}

/* A status row reads as a timeline entry: the result is the leading signal. */
.rec-lead,
.row-status {
  border-left: var(--rec-lead-w) solid var(--border);
}

.rec-lead.ok,
.row-status.ok {
  border-left-color: var(--ok);
}

.rec-lead.denied,
.rec-lead.warning,
.row-status.denied,
.row-status.warning {
  border-left-color: var(--warn);
}

.rec-lead.error,
.row-status.error {
  border-left-color: var(--err);
}

.rec-lead.guest,
.row-status.guest {
  border-left-color: var(--accent);
}

/* --------------------------------------------------------- share block --- */

.share-url {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--input-bg);
  border: var(--border-w) solid var(--input-border);
  border-radius: var(--radius-md);
  padding-left: var(--space-3);
  min-height: var(--control-h);
  overflow: hidden;
}

.share-url code,
.share-url input {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: none;
  border: none;
  padding: 0;
  min-height: 0;
}

.share-url input:focus {
  outline: none;
  box-shadow: none;
}

.share-url .btn {
  border-radius: 0;
  border-width: 0 0 0 var(--border-w);
  align-self: stretch;
  flex: 0 0 auto;
}

.qr-frame {
  background: #fff;
  border-radius: var(--radius-md);
  padding: 10px;
  line-height: 0;
  flex: 0 0 auto;
}

.share-hero {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}

/* The freshly-created link is the one card on the page that must not be
   mistaken for a list item. */
.share-card {
  border-color: var(--accent-border);
}

/* --------------------------------------------------------- door picker ---

   The old form gave every door a checkbox, a number input and a nested
   <details> on one wrapping flex row. On a phone that collapsed into four
   ragged lines per door. Here the door is a single full-width toggle, and its
   limits only appear once it is actually switched on.                        */

.picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.pick {
  border: var(--border-w) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  overflow: hidden;
}

.pick.on {
  border-color: var(--accent-border);
  background: var(--accent-wash);
}

.pick-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: var(--control-h);
  padding: var(--space-3);
  margin: 0;
  cursor: pointer;
  font-weight: 600;
  font-size: var(--text-sm);
}

.pick-toggle input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.switch {
  position: relative;
  flex: 0 0 auto;
  width: 44px;
  height: 26px;
  border-radius: var(--radius-pill);
  background: var(--switch-off);
  border: var(--border-w) solid var(--border);
  transition: background-color 0.16s;
}

.switch::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-pill);
  background: var(--switch-knob);
  transition: transform 0.16s;
}

.pick-toggle input:checked ~ .switch {
  background: var(--accent);
  border-color: var(--accent);
}

.pick-toggle input:checked ~ .switch::after {
  transform: translateX(18px);
  background: var(--accent-ink);
}

.pick-toggle input:focus-visible ~ .switch {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

.pick-body {
  display: none;
  padding: 0 var(--space-3) var(--space-3);
  border-top: var(--border-w) solid var(--border);
  padding-top: var(--space-3);
}

.pick.on .pick-body {
  display: block;
}

/* Seven weekdays share a phone's width, so they are a fixed 7-track grid. As a
   wrapping flex row the last day dropped to its own line at larger control
   sizes. */
.chip-row {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
}

.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  min-height: var(--control-h);
  padding: 0 2px;
  border-radius: var(--radius-sm);
  border: var(--border-w) solid var(--border);
  background: var(--input-bg);
  color: var(--text-2);
  font-size: var(--text-xs);
  font-weight: 650;
  cursor: pointer;
  user-select: none;
}

.chip input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}

.chip:has(input:checked) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.chip:has(input:focus-visible) {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

.time-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.time-row input {
  width: 92px;
  text-align: center;
  font-family: var(--font-mono);
}

/* -------------------------------------------------------------- wizard --- */

.steps {
  display: flex;
  gap: 6px;
  margin-bottom: var(--space-4);
}

.step {
  flex: 1;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--border);
}

.step.done {
  background: var(--accent);
}

.step-panel {
  display: none;
}

.step-panel.active {
  display: block;
}

.step-nav {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.step-nav .btn {
  flex: 1;
}

.step-label {
  font-size: var(--text-eyebrow);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-3);
  margin-bottom: 6px;
}

/* ---------------------------------------------------------- filter bar --- */

/* Wraps rather than scrolls. A horizontal scroller left the last filter sliced
   in half at the screen edge, which reads as a rendering bug — and a filter
   you cannot see is a filter you never apply. */
.filterbar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-2);
}

.filterbar select {
  width: 100%;
  font-size: var(--text-xs);
  min-height: 42px;
}

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

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 650;
  letter-spacing: 0.02em;
  border: var(--border-w) solid var(--border);
  background: var(--badge-bg);
  color: var(--text-2);
  white-space: nowrap;
}

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

.badge-warning,
.badge-warn {
  color: var(--warn-text);
  border-color: var(--warn-border);
  background: var(--warn-wash);
}

.badge-error,
.badge-err {
  color: var(--err-text);
  border-color: var(--err-border);
  background: var(--err-wash);
}

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

.dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: currentColor;
  flex: 0 0 auto;
}

/* --------------------------------------------------------------- toast --- */

#toast {
  position: fixed;
  bottom: calc(22px + env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  background: var(--surface);
  border: var(--border-w) solid var(--border);
  color: var(--text);
  padding: 12px 18px;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s;
  max-width: 90%;
  z-index: 100;
  box-shadow: var(--shadow-1);
}

#toast.show {
  opacity: 1;
}

#toast.ok {
  border-left: 3px solid var(--ok);
}

#toast.err {
  border-left: 3px solid var(--err);
}

/* -------------------------------------------------- PWA install banner --- */

.install-banner {
  position: fixed;
  left: max(16px, env(safe-area-inset-left));
  bottom: calc(16px + env(safe-area-inset-bottom));
  z-index: 90;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: calc(100% - 32px);
  background: var(--surface);
  border: var(--border-w) solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  box-shadow: var(--shadow-1);
}

.install-banner[hidden] {
  display: none;
}

.install-text {
  font-size: var(--text-xs);
  color: var(--text-2);
}

.install-dismiss {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-3);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.install-dismiss:hover {
  color: var(--text);
}

@media (max-width: 640px) {
  .install-banner {
    left: 12px;
    right: 12px;
    max-width: none;
  }
  .install-text {
    flex: 1;
  }
}

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

   Below the breakpoint the record list above replaces these entirely, so the
   old per-cell data-label restack is gone: a record is one object, not seven
   labelled lines.                                                            */

.table-wrap {
  display: none;
}

@media (min-width: 860px) {
  .table-wrap {
    display: block;
    overflow-x: auto;
    border: var(--border-w) solid var(--border);
    border-radius: var(--radius-md);
  }

  .card-flush .table-wrap {
    border: none;
    border-radius: 0;
  }

  .records {
    display: none;
  }

  table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
  }

  thead {
    background: var(--thead-bg);
  }

  th {
    text-align: left;
    font-size: var(--text-eyebrow);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-3);
    padding: 10px var(--space-4);
    border-bottom: var(--border-w) solid var(--border);
    white-space: nowrap;
  }

  td {
    padding: 12px var(--space-4);
    border-bottom: var(--border-w) solid var(--border);
    color: var(--text);
    vertical-align: middle;
  }

  tbody tr:last-child td {
    border-bottom: none;
  }

  tbody tr:nth-child(even) {
    background: var(--wash);
  }
}

/* ---------------------------------------------------------- login card --- */

/* Full-bleed page with no nav above it, so it takes all four insets itself. */
.login-warm {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding:
    max(24px, env(safe-area-inset-top))
    max(24px, env(safe-area-inset-right))
    max(24px, env(safe-area-inset-bottom))
    max(24px, env(safe-area-inset-left));
  background:
    radial-gradient(1100px 560px at 50% -10%, var(--surface) 0%, transparent 62%),
    var(--bg);
}

.login-warm-card {
  width: 100%;
  max-width: 22rem;
  text-align: center;
  background: var(--surface);
  border: var(--border-w) solid var(--border);
  border-radius: var(--radius-lg);
  padding: 38px 30px 26px;
  box-shadow: var(--shadow-1);
}

/* The mark at the top of every full-screen card. Same size, radius and lift as
   the door plates on the dashboard, so the screen you sign in on is visibly
   about the thing you came here to do — and so the app looks like one product
   to a guest, who only ever sees these screens. */
.login-mark {
  width: var(--thumb-size);
  height: var(--thumb-size);
  margin: 0 auto var(--space-3);
  border-radius: var(--radius-thumb);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-wash);
  color: var(--accent-text);
  border: var(--border-w) solid var(--accent-border);
}

/* The screens you arrive on to do something get the full plate. */
.login-mark-solid {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: transparent;
  box-shadow: var(--shadow-thumb);
}

/* A dead or paused link is not good news, so it does not get the warm plate. */
.login-mark-muted {
  background: var(--surface-2);
  color: var(--text-3);
  border-color: var(--border);
}

.login-brand {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0 0 var(--space-1);
}

/* One block, one gap. The header used to hand off to the form on a 4px margin,
   which left greeting, labels and inputs reading as a single undifferentiated
   stack. */
.login-head {
  margin-bottom: var(--space-5);
}

.login-hi {
  font-family: var(--font-display);
  font-size: var(--text-h1);
  line-height: 1.2;
  letter-spacing: var(--tracking-display);
  color: var(--text);
  margin: 0 0 var(--space-1);
}

.login-tagline {
  color: var(--text-2);
  font-size: var(--text-sm);
  line-height: 1.5;
  margin: 0;
  text-wrap: balance;
}

.login-link-validity {
  color: var(--text-3);
  font-size: var(--text-xs);
  margin: var(--space-2) 0 0;
}

/* A control living inside the field, sized to fill it — a text button in the
   label row would have been a 16px-tall tap target. */
.input-affix {
  position: relative;
  display: flex;
  align-items: center;
}

.input-affix input {
  padding-right: 5.5rem;
}

.affix-btn {
  position: absolute;
  top: var(--border-w);
  right: var(--border-w);
  bottom: var(--border-w);
  display: flex;
  align-items: center;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  background: none;
  color: var(--accent-text);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
}

.affix-btn:hover {
  color: var(--accent-hover);
}

.login-warm-card .field {
  text-align: left;
  margin-bottom: var(--space-3);
}

.login-warm-card label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 6px;
}

.btn-warm {
  margin-top: var(--space-2);
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent);
}

.btn-warm:hover {
  background: var(--accent-hover);
}

/* A submitting form must not look idle — without this the button sits there
   unchanged while the request is in flight and people press it twice. */
.btn-warm[disabled] {
  opacity: 0.72;
  cursor: progress;
}

/* The mark leads the line rather than trailing it. As a trailing emoji it was
   the one thing that could wrap alone, and it did — a key sitting by itself on
   a second line. Leading it also means the text wraps like ordinary text. */
.login-foot {
  color: var(--text-3);
  font-size: var(--text-xs);
  line-height: 1.6;
  margin: var(--space-4) 0 0;
  text-wrap: balance;
}

.login-foot .icon {
  vertical-align: -2px;
  margin-right: 4px;
  color: var(--text-3);
}

/* password rules (change_password) — the same list the server enforces, ticked
   live by JS. Colour alone never carries the state: the mark changes too. */
.rule-list {
  list-style: none;
  margin: -4px 0 16px;
  padding: 0;
  text-align: left;
}

.rule {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-3);
  font-size: var(--text-xs);
  line-height: 1.9;
}

.rule-mark {
  flex: 0 0 1em;
  text-align: center;
}

.rule.is-met {
  color: var(--ok-text);
}

/* guest door list (link_landing) — inherits the shared door styling */
.login-warm-card .door-list {
  margin-top: 6px;
  text-align: left;
}

/* The guest card has no grip or star, so the plate keeps its own padding. */
.login-warm-card .door-btn {
  padding-right: var(--space-4);
}

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

/* Keyboard-only escape hatch past the nav, revealed on focus. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--surface);
  color: var(--text);
  border: var(--border-w) solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: var(--text-sm);
}

.skip-link:focus {
  left: 8px;
  top: 8px;
}

.error-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding:
    max(24px, env(safe-area-inset-top))
    max(24px, env(safe-area-inset-right))
    max(24px, env(safe-area-inset-bottom))
    max(24px, env(safe-area-inset-left));
}

.error-page .card {
  max-width: 26rem;
  text-align: center;
}

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