/* rezerwacje — one stylesheet, no preprocessor.
 *
 * Mobile-first: the common case is somebody booking a desk on a phone the evening
 * before. Desktop gets more columns, not a different layout.
 *
 * The design system is ported from the DSK CRM kit (frontend-template/frontend.md),
 * but NOT its stack. That kit is Django + HTMX + Alpine + Tailwind 3 with an npm
 * build; this client is buildless ES modules against a JSON API and must stay
 * editable on the server. So what crosses over is the *design*: the token system
 * (colour ramps as space-separated RGB triplets, type scale, radius, shadow,
 * easing, z-index ladder), the component contracts, and the gotchas in its §13.
 * What does not cross over is Tailwind, the utility classes, the templatetags and
 * every library it names. Section numbers in comments below refer to that document.
 */

/* ---- fonts ----------------------------------------------------------------
 * Self-hosted rather than linked from Google Fonts. The kit's §1 rule is "no CDNs
 * at runtime", and it vendors libraries through npm -- which does not exist here,
 * so the woff2 files are simply committed under app/fonts/ and served by Caddy off
 * the same origin as everything else. This matters most for /tablet: a door display
 * that runs unattended for months must not depend on fonts.googleapis.com resolving.
 *
 * Both families are variable fonts, so one file covers the whole weight axis.
 * `latin-ext` carries the Polish diacritics (ą ć ę ł ń ś ź ż -- ó is in `latin`);
 * the unicode-range split is Google's own, so a browser only fetches the second
 * file once a diacritic is actually rendered.
 *
 * Licence: both are SIL Open Font License 1.1. See app/fonts/README.md.
 */

@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('fonts/fraunces-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: 'Fraunces';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('fonts/fraunces-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;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('fonts/manrope-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: 'Manrope';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('fonts/manrope-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;
}

/* ---- layer 1: raw ramps ----------------------------------------------------
 * Kit §2.2: colours are stored as SPACE-SEPARATED RGB TRIPLETS, never hex, so the
 * same token serves an opaque fill and an alpha one without a second source of
 * truth: `rgb(var(--accent-500))` and `rgb(var(--accent-500) / .25)`. In the kit
 * that also feeds Tailwind's `<alpha-value>`; here it just removes every
 * `color-mix()` and every `rgba(…)` literal from the component rules.
 *
 * Nothing outside this block should name a hue. Rebranding is editing --accent-*.
 */

:root {
  /* Accent — carries interaction: hover, focus, active, branding. Orange, from the
   * kit. It was blue #1f6feb, which was byte-identical to --mine: a *selected* desk
   * and an *owned* desk were the same colour. Orange is the one hue the floor map's
   * status palette (green free / blue mine / red taken / violet assigned) does not
   * already spend, so the accent no longer competes with a desk state. */
  --accent-50: 255 247 237;
  --accent-100: 255 237 213;
  --accent-200: 254 215 170;
  --accent-300: 253 186 116;
  --accent-400: 251 146 60;
  --accent-500: 249 115 22;
  --accent-600: 234 88 12;
  --accent-700: 194 65 12;

  /* Gold — the accent's partner in gradients and ornaments only, never a control. */
  --gold-400: 251 191 36;
  --gold-500: 245 158 11;
  --gold-600: 217 119 6;

  /* Neutrals — everything structural. */
  --gray-50: 249 250 251;
  --gray-100: 243 244 246;
  --gray-200: 229 231 235;
  --gray-300: 209 213 219;
  --gray-400: 156 163 175;
  --gray-500: 107 114 128;
  --gray-600: 75 85 99;
  --gray-700: 55 65 81;
  --gray-800: 31 41 55;
  --gray-900: 17 24 39;
  --gray-950: 3 7 18;

  /* Status hues. Kit §6.11: assigned once, centrally, never re-picked per screen.
   * The mapping to desk states is fixed by the product, not by taste:
   *   emerald = free · blue = yours · red = taken · violet = assigned to someone
   *   amber   = caution (a stale plan, a check-in cutoff) · gray = inactive */
  --emerald-50: 236 253 245;
  --emerald-400: 52 211 153;
  --emerald-500: 16 185 129;
  --emerald-600: 5 150 105;
  --emerald-700: 4 120 87;

  --blue-50: 239 246 255;
  --blue-400: 96 165 250;
  --blue-500: 59 130 246;
  --blue-600: 37 99 235;
  --blue-700: 29 78 216;

  --red-50: 254 242 242;
  --red-400: 248 113 113;
  --red-500: 239 68 68;
  --red-600: 220 38 38;
  --red-700: 185 28 28;

  --violet-50: 245 243 255;
  --violet-400: 167 139 250;
  --violet-500: 139 92 246;
  --violet-600: 124 58 237;
  --violet-700: 109 40 217;

  --amber-50: 255 251 235;
  --amber-400: 251 191 36;
  --amber-500: 245 158 11;
  --amber-600: 217 119 6;
  --amber-700: 180 83 9;
}

/* ---- layer 2: semantic aliases ---------------------------------------------
 * The names the app actually uses. Several are read from JavaScript as literal
 * strings -- map.js paints desks with 'var(--free-soft)' and floor.js builds the
 * legend from the same tokens (constraint 2: desks are styled by CSS selector, the
 * artifact is never redrawn) -- so these names are an interface, not an internal
 * detail. Re-point them at different ramp steps freely; do not rename them.
 */

:root {
  --bg: rgb(var(--gray-50));
  --surface: 255 255 255;
  --surface-2: rgb(var(--gray-100));
  --ink: rgb(var(--gray-900));
  --ink-2: rgb(var(--gray-500));
  --line: rgb(var(--gray-950) / .10);
  --line-solid: rgb(var(--gray-200));

  --accent: rgb(var(--accent-500));
  --accent-strong: rgb(var(--accent-600));
  --accent-ink: #ffffff;

  /* The floor-map / calendar status palette. `-soft` is the fill, the bare token is
   * the stroke and the text colour, so each pair has to hold contrast on its own. */
  --free: rgb(var(--emerald-600));
  --free-soft: rgb(var(--emerald-50));
  --mine: rgb(var(--blue-600));
  --mine-soft: rgb(var(--blue-50));
  --taken: rgb(var(--red-600));
  --taken-soft: rgb(var(--red-50));
  --fixed: rgb(var(--violet-600));
  --fixed-soft: rgb(var(--violet-50));
  --off: rgb(var(--gray-400));
  --off-soft: rgb(var(--gray-100));
  --warn: rgb(var(--amber-700));
  --warn-soft: rgb(var(--amber-50));

  /* ---- the floor plan's own palette -------------------------------------
   * These are the colours map.js paints desks and rooms with, and floor.js builds
   * the legend from. They are SEPARATE from the --free / --mine / --taken pairs
   * above, and they are deliberately NOT re-pointed under data-theme="dark".
   *
   * The reason is that the plan is an artifact, not chrome. It is the architect's
   * drawing, inlined and tinted by selector (constraint 2), and its rooms, walls and
   * zone fills are light in every theme -- the one way to darken it would be a CSS
   * `filter` on the SVG, which constraint 10 rules out because it pins the plan to a
   * single fit-to-width rasterisation and makes a zoomed map blurry. So in dark mode
   * the plan reads as a sheet of light paper on a dark page, and a desk mark has to
   * stay legible ON THAT PAPER.
   *
   * Wiring the theme toggle is what surfaced this: the dark `-soft` tokens are
   * low-alpha washes tuned for a dark surface, and painting them onto the plan
   * composited them against whatever zone fill sat underneath -- so the same status
   * came out a different muddy colour in the lavender zone than in the pink one.
   * The page tokens above stay alpha; these stay opaque and light-tuned.
   */
  --plan-free: rgb(var(--emerald-600));
  --plan-free-soft: rgb(var(--emerald-50));
  --plan-mine: rgb(var(--blue-600));
  --plan-mine-ink: #ffffff;
  --plan-taken: rgb(var(--red-600));
  --plan-taken-soft: rgb(var(--red-50));
  --plan-fixed: rgb(var(--violet-600));
  --plan-fixed-soft: rgb(var(--violet-50));
  --plan-off: rgb(var(--gray-400));
  --plan-off-soft: rgb(var(--gray-100));
  /* The ring around the selected desk. On the plan, so light-tuned and theme-fixed
   * for the same reason as the rest of this block -- and accent-600 rather than
   * accent-500 so it separates from the red of a taken desk at phone size. */
  --plan-selected: rgb(var(--accent-600));

  /* Deliberately NOT --accent. Two marks sit directly on top of the status palette:
   * the ring around a selected desk, and the "Teraz" line on /sale, which is drawn
   * across lanes full of pale-red booking blocks. Both need to stay legible against
   * red, and both need to not read AS a status. Keeping them on their own token
   * means a future accent change cannot quietly make either unreadable. */
  --marker: rgb(var(--accent-600));
  --marker-ink: #ffffff;

  /* Type — kit §2.1. Display is for page titles and the tablet's hero state, and
   * nothing else; body carries the rest. */
  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font-body: 'Manrope', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  /* Root stays 16px. The kit runs at 14px for a denser desktop CRM, but this is a
   * thumb-first app, and 16px is also why it needs none of the kit's §13 gotcha 9
   * workaround -- iOS Safari zooms on focus for any input under 16px, and inputs
   * here inherit the root. Density comes from the scale below, not from the root. */
  --text-micro: .6875rem;   /* 11px — uppercase micro-labels */
  --text-xs: .75rem;        /* 12px — calendar blocks, day-cell subtext */
  --text-sm: .8125rem;      /* 13px — meta, helper text, legend */
  --text-md: .9375rem;      /* 15px — secondary body */
  --text-base: 1rem;        /* 16px — body */

  /* Radius — kit §2.3. */
  --radius-pill: 999px;
  --radius-control: .5rem;
  --radius-card: .75rem;
  --radius-hero: 1.25rem;
  --radius: var(--radius-card);

  /* Shadow — kit §2.3. `lift` carries an accent tint so a raised surface reads as
   * "the app did that", not as a drop shadow. */
  --shadow-rest: 0 1px 2px rgb(var(--gray-950) / .04), 0 1px 3px rgb(var(--gray-950) / .05);
  --shadow-lift: 0 10px 20px rgb(var(--accent-500) / .12),
                 0 3px 8px rgb(var(--gray-950) / .07),
                 0 0 0 1px rgb(var(--accent-500) / .14);
  --shadow-panel: 0 10px 25px -5px rgb(var(--gray-950) / .10),
                  0 4px 10px -3px rgb(var(--gray-950) / .05);
  --shadow: var(--shadow-rest);

  /* Easing + duration ladder — kit §2.3. */
  --ease-standard: cubic-bezier(.4, 0, .2, 1);
  --ease-spring: cubic-bezier(.34, 1.56, .64, 1);
  --ease-luxury: cubic-bezier(.16, 1, .3, 1);
  --dur-fast: 120ms;
  --dur: 200ms;
  --dur-slow: 300ms;

  /* Z-index ladder — kit §2.4. Fixed and global: adding a layer means picking a
   * slot here, not inventing a number. The calendar's own 1/2/3 are local to
   * .cal-scroll's stacking context and are not part of this ladder. */
  --z-sticky: 30;
  --z-float: 50;
  --z-skip: 10000;

  --tap: 44px;

  /* The one glyph in the stylesheet, used as a mask by the empty state. Inline so it
   * costs no request, and stroked in black because a mask reads alpha, not colour --
   * the fill comes from whatever paints the element. */
  --glyph-calendar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
  --glyph-sun: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='4.2'/%3E%3Cpath d='M12 2.2v2.1M12 19.7v2.1M4.1 4.1l1.5 1.5M18.4 18.4l1.5 1.5M2.2 12h2.1M19.7 12h2.1M4.1 19.9l1.5-1.5M18.4 5.6l1.5-1.5'/%3E%3C/svg%3E");
  --glyph-moon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linejoin='round'%3E%3Cpath d='M20.8 13.4A8.8 8.8 0 1 1 10.6 3.2a6.9 6.9 0 0 0 10.2 10.2z'/%3E%3C/svg%3E");

  /* Light by default, and deliberately NOT `light dark`: the app does not follow the
   * operating system's preference, it follows the toggle in the header. Native
   * controls (date inputs, selects, scrollbars) follow `color-scheme`, so leaving it
   * at `light dark` would render a dark date picker inside a light page on a
   * dark-mode device that had chosen the light theme. */
  color-scheme: light;
}

/* ---- dark ------------------------------------------------------------------
 * Reached only by data-theme="dark" on <html>, set by the toggle in the header and
 * re-applied before first paint by the blocking script in index.html (kit §4).
 * Only layer 2 is re-pointed; the ramps above are absolute.
 *
 * Kit §11: the accent shifts one step lighter on dark surfaces (accent-500 on
 * gray-800 fails contrast), backgrounds step UP the gray scale rather than down,
 * and shadow opacity roughly doubles because a shadow tuned on white is invisible
 * on gray-900.
 */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: rgb(var(--gray-950));
  --surface: 23 31 45;
  --surface-2: rgb(var(--gray-800));
  --ink: rgb(var(--gray-100));
  --ink-2: rgb(var(--gray-400));
  --line: rgb(255 255 255 / .12);
  --line-solid: rgb(var(--gray-700));

  --accent: rgb(var(--accent-400));
  --accent-strong: rgb(var(--accent-300));
  --accent-ink: rgb(var(--gray-950));

  /* `-soft` fills invert from a 50-tint to a low-alpha wash of the hue itself: a
   * pale tint would be lighter than the surface it sits on and read as raised. */
  --free: rgb(var(--emerald-400));
  --free-soft: rgb(var(--emerald-500) / .16);
  --mine: rgb(var(--blue-400));
  --mine-soft: rgb(var(--blue-500) / .20);
  --taken: rgb(var(--red-400));
  --taken-soft: rgb(var(--red-500) / .18);
  --fixed: rgb(var(--violet-400));
  --fixed-soft: rgb(var(--violet-500) / .20);
  --off: rgb(var(--gray-500));
  --off-soft: rgb(var(--gray-500) / .18);
  --warn: rgb(var(--amber-400));
  --warn-soft: rgb(var(--amber-500) / .16);

  --marker: rgb(var(--accent-300));
  --marker-ink: rgb(var(--gray-950));

  --shadow-rest: 0 1px 2px rgb(0 0 0 / .40), 0 2px 6px rgb(0 0 0 / .30);
  --shadow-lift: 0 10px 24px rgb(0 0 0 / .45),
                 0 3px 8px rgb(0 0 0 / .35),
                 0 0 0 1px rgb(var(--accent-400) / .25);
  --shadow-panel: 0 12px 28px -6px rgb(0 0 0 / .55), 0 4px 12px -3px rgb(0 0 0 / .40);
}

/* ---- reset ----------------------------------------------------------------- */

* { box-sizing: border-box; }

/* The `hidden` attribute hides an element only through the UA stylesheet rule
 * `[hidden] { display: none }`, and ANY author `display` declaration outranks it --
 * `#chrome` sets `display: flex` from an id selector. Without this rule the
 * `chrome.hidden = ...` assignments in router.js are silently inert, and the login,
 * forced-password-change and logged-out 404 screens all render a full nav bar with a
 * "Wyloguj" button for someone who is not logged in.
 * `!important` so it keeps holding whatever display type a future element is given. */
[hidden] { display: none !important; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--text-base)/1.55 var(--font-body);
  font-weight: 400;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ---- type ------------------------------------------------------------------
 * Kit §2.1: the display face is for page titles and nothing else. Every screen's
 * <h1> gets it, plus the accent→gold hairline that ends the page header in the kit
 * -- rendered here as an ::after on the heading itself, because this client has no
 * page-header component to hang it on.
 */

h1, h2, h3 { line-height: 1.2; margin: 0 0 .5rem; }

h1 {
  font-family: var(--font-display);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -.02em;
  font-optical-sizing: auto;
}
/* Not on the tablet: .tablet-room is an <h1> too, but it is a flex item sized to its
 * own text, so the rule drew a short orange stub under the room name that read as a
 * stray underline rather than as a page-header rule. The door display's structure is
 * the colour bar and the hero word, and it does not want a second ornament. */
main h1:not(.tablet-room)::after {
  content: '';
  display: block;
  width: 100%; height: 1px;
  margin-top: .55rem;
  background: linear-gradient(90deg,
    rgb(var(--accent-500) / .45), rgb(var(--gold-400) / .22), transparent);
}
h2 { font-size: 1.125rem; font-weight: 650; letter-spacing: -.01em; }
h3 { font-size: 1rem; font-weight: 650; }
p { margin: .35rem 0; }

a { color: var(--accent); text-underline-offset: 2px; }

.skip {
  position: absolute; left: -9999px;
  z-index: var(--z-skip);
  background: rgb(var(--surface)); color: var(--ink);
  padding: .75rem 1rem;
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-panel);
}
.skip:focus { left: .5rem; top: .5rem; }

/* Kit §12: focus is always visible, and `outline: none` is only ever paired with a
 * replacement ring. Nothing here removes it. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ---- chrome --------------------------------------------------------------- */

/* Glass rather than solid, which is the kit's §5.3 treatment for the top nav. The
 * fallback matters: without backdrop-filter support this is a flat surface, not a
 * transparent one, so the map never shows through the header. */
#chrome {
  position: sticky; top: 0; z-index: var(--z-sticky);
  display: flex; align-items: center; gap: .5rem;
  padding: .35rem max(.5rem, env(safe-area-inset-left)) .35rem max(.5rem, env(safe-area-inset-right));
  background: rgb(var(--surface));
  border-bottom: 1px solid var(--line);
}
/* The kit's §5.3 nav is glass at a much lower opacity, but it is a desktop CRM with
 * a single-row header. Here the header wraps to two rows below 700px and the top row
 * carries "Wyloguj", and at .78 the page text scrolling underneath stayed legible
 * straight through it -- the blur softens it without hiding it. .92 keeps the depth
 * cue and the saturation lift while reading as a surface rather than a window. */
@supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
  #chrome {
    background: rgb(var(--surface) / .92);
    -webkit-backdrop-filter: blur(12px) saturate(1.6);
    backdrop-filter: blur(12px) saturate(1.6);
  }
}
#chrome nav {
  display: flex; gap: .15rem; overflow-x: auto; flex: 1;
  scrollbar-width: none;
}
#chrome nav::-webkit-scrollbar { display: none; }

/* Kit §5.3: the active state is the hover state made permanent. */
#chrome nav a {
  flex: 0 0 auto;
  padding: .55rem .7rem;
  border-radius: var(--radius-control);
  color: var(--ink-2);
  text-decoration: none;
  font-size: var(--text-md);
  font-weight: 550;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard);
}
@media (hover: hover) {
  #chrome nav a:hover { background: rgb(var(--gray-950) / .05); color: var(--ink); }
  :root[data-theme="dark"] #chrome nav a:hover { background: rgb(255 255 255 / .06); }
}
#chrome nav a.active {
  background: rgb(var(--accent-500) / .12);
  color: var(--accent-strong);
  font-weight: 650;
}
:root[data-theme="dark"] #chrome nav a.active { color: var(--accent); }

.chrome-right { display: flex; align-items: center; gap: .4rem; }
.whoami {
  font-size: var(--text-sm); color: var(--ink-2); font-weight: 500;
  max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (min-width: 700px) { .whoami { max-width: 22ch; } }

/* Below 700px the four nav labels, the name and the logout button cannot share one
 * row: at 375px they need ~460px. The nav is `overflow-x: auto` with the scrollbar
 * suppressed, so the surplus was scrolled out of sight with nothing to indicate it --
 * "Moje rezerwacje" read as "Moje re" and collided with the name. Giving the nav its
 * own full-width row fits all four labels honestly. The horizontal scroll stays as
 * the fallback for a future fifth item or a very narrow device. */
@media (max-width: 699px) {
  #chrome { flex-wrap: wrap; row-gap: .1rem; }
  .chrome-right { order: 1; margin-left: auto; }
  /* `wrap` rather than relying on the nav's own overflow: with five tabs a very
   * narrow device can still run out of room, and a second line is visible where a
   * suppressed scrollbar is not. */
  #chrome nav { order: 2; flex: 1 0 100%; flex-wrap: wrap; overflow-x: visible; }
  #chrome nav a { padding: .5rem .55rem; }
  .whoami { max-width: none; }
}

body.bare #chrome { display: none; }

/* The theme toggle. Icon-only, so it carries an aria-label from main.js; the two
 * glyphs are swapped there too rather than by CSS, because the button's accessible
 * name has to change with them. */
.theme-toggle {
  flex: 0 0 auto;
  width: 36px; height: 36px;
  display: inline-grid; place-items: center;
  padding: 0;
  border: 1px solid transparent; border-radius: var(--radius-pill);
  background: transparent; color: var(--ink-2);
  font: inherit; font-size: 1rem; line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-standard),
              color var(--dur) var(--ease-standard),
              transform var(--dur) var(--ease-spring);
}
@media (hover: hover) {
  .theme-toggle:hover {
    background: rgb(var(--accent-500) / .12);
    color: var(--accent-strong);
    transform: translateY(-1px);
  }
  :root[data-theme="dark"] .theme-toggle:hover { color: var(--accent); }
}

/* The icon shows the theme you would GET, matching the button's aria-label, which
 * names the action rather than the state. Swapping it here off `data-theme` rather
 * than from theme.js keeps the switch to a single attribute write. */
.theme-toggle-icon {
  width: 1.15rem; height: 1.15rem;
  background: currentColor;
  -webkit-mask: var(--glyph-moon) no-repeat center / contain;
  mask: var(--glyph-moon) no-repeat center / contain;
}
:root[data-theme="dark"] .theme-toggle-icon {
  -webkit-mask-image: var(--glyph-sun);
  mask-image: var(--glyph-sun);
}

/* Present to assistive technology and to the document outline, absent from the
 * layout. The map screen uses it for its <h1>: the nav already says "Plan biura", so
 * spending 3 rem of a phone on repeating it costs map. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- the map screen claims the viewport -----------------------------------
 * Every other screen is a document that scrolls. This one is a map: it fills the
 * space under the header and the plan fits inside whatever shape that is
 * (preserveAspectRatio="xMidYMid meet"), rather than the page scrolling around a
 * fixed-ratio box that leaves the plan two inches tall on a phone. The class is set
 * by screens/floor.js on mount and removed on teardown.
 */
body.map-screen { display: flex; flex-direction: column; min-height: 100dvh; }
body.map-screen main {
  flex: 1 1 auto;
  display: flex; flex-direction: column;
  /* min-height:0 so a flex child may be SHORTER than its content -- without it the
   * map refuses to shrink and pushes the legend off-screen. */
  min-height: 0;
  padding-bottom: 0;
}
body.map-screen .floor-screen {
  flex: 1 1 auto;
  display: flex; flex-direction: column;
  min-height: 0;
}
body.map-screen .floor-bar,
body.map-screen .legend { flex: 0 0 auto; }
body.map-screen .mapwrap { flex: 1 1 auto; min-height: 9rem; }
/* The container is now whatever height is left, so the plan's own ratio must not
 * dictate it -- the SVG letterboxes itself inside the box instead. */
body.map-screen .mapview { aspect-ratio: auto; height: 100%; }

main { padding-bottom: 3rem; }
.pad { padding: 1rem max(.75rem, env(safe-area-inset-left)) 1rem max(.75rem, env(safe-area-inset-right)); }
.narrow { max-width: 26rem; margin: 0 auto; }
.wide { max-width: 60rem; margin: 0 auto; }

/* ---- buttons ---------------------------------------------------------------
 * Kit §6.1. Four variants, pills except the destructive one. `.btn` is the app's
 * emphasised action (book, save, log in) and so maps to the kit's
 * `.btn-primary-accent`: solid accent fill, inset ring, lifts 1px on hover.
 * `.btn-quiet` is its `.btn-secondary`.
 */

.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  min-height: var(--tap); padding: .55rem 1.15rem;
  border: 0; border-radius: var(--radius-pill);
  background: var(--accent); color: var(--accent-ink);
  box-shadow: inset 0 0 0 1px rgb(var(--gray-950) / .06),
              0 1px 2px rgb(var(--accent-600) / .30);
  font: inherit; font-weight: 650; letter-spacing: .01em;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-standard),
              box-shadow var(--dur) var(--ease-standard),
              transform var(--dur) var(--ease-spring);
}
@media (hover: hover) {
  .btn:hover:not(:disabled) {
    background: var(--accent-strong);
    transform: translateY(-1px);
    box-shadow: inset 0 0 0 1px rgb(var(--gray-950) / .08),
                0 4px 12px rgb(var(--accent-500) / .35);
  }
}
.btn:active:not(:disabled) { transform: translateY(0); }

/* Kit §12: the disabled state is the attribute AND a visual, never one alone. */
.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }

.btn-quiet {
  background: transparent; color: var(--ink-2);
  box-shadow: inset 0 0 0 1px var(--line);
  font-weight: 550;
}
@media (hover: hover) {
  .btn-quiet:hover:not(:disabled) {
    background: rgb(var(--accent-500) / .08);
    color: var(--accent-strong);
    box-shadow: inset 0 0 0 1px rgb(var(--accent-500) / .35);
  }
  :root[data-theme="dark"] .btn-quiet:hover:not(:disabled) { color: var(--accent); }
}

/* Kit §6.1: destructive actions are red and NEVER a pill -- deliberately a different
 * shape from the accent family so they cannot be clicked on autopilot.
 *
 * Used ONLY on the administration screens, and the split is the point rather than an
 * accident of which screen was written last. The destructive-ish actions on the staff
 * screens are "Anuluj" and "Zwolnij", and both are routine and reversible -- you
 * cancel a desk and book another one -- so painting them red every day would cry
 * wolf, and they stay `.btn-quiet`.
 *
 * What red is reserved for is an action taken on somebody ELSE and not undone by
 * repeating it: deactivating an account, revoking a tablet token, cancelling a
 * colleague's booking. Those are the kit's "deleting a client record" case, and the
 * different SHAPE matters as much as the colour -- a square button in a row of pills
 * cannot be hit on autopilot by muscle memory aimed at the pill beside it. */
.btn-danger {
  border-radius: var(--radius-control);
  background: rgb(var(--red-600)); color: #fff;
  box-shadow: inset 0 0 0 1px rgb(var(--gray-950) / .08);
}
@media (hover: hover) {
  .btn-danger:hover:not(:disabled) {
    background: rgb(var(--red-700));
    box-shadow: inset 0 0 0 1px rgb(var(--gray-950) / .10),
                0 4px 12px rgb(var(--red-600) / .35);
  }
}
:root[data-theme="dark"] .btn-danger { background: rgb(var(--red-500)); }

.btn-small { min-height: 34px; padding: .3rem .8rem; font-size: var(--text-sm); }
.btn-block { width: 100%; }

/* ---- forms -----------------------------------------------------------------
 * Kit §6.3. The kit puts six `.form-*` classes on every field; this client styles
 * the elements directly, which reaches the same end -- there is no ad-hoc input
 * styling anywhere -- without asking every call site to remember a class name.
 */

.field { margin: 0 0 .9rem; }
.field label {
  display: block;
  font-size: var(--text-sm); font-weight: 650; color: var(--ink);
  margin-bottom: .35rem;
}
.field small { display: block; margin-top: .35rem; font-size: var(--text-sm); color: var(--ink-2); }

input[type=text], input[type=email], input[type=password], input[type=date],
input[type=time], input[type=search], input[type=number], select, textarea {
  width: 100%; min-height: var(--tap);
  padding: .55rem .75rem;
  border: 1px solid var(--line-solid); border-radius: var(--radius-control);
  background: rgb(var(--surface)); color: var(--ink);
  font: inherit;
  /* Kit §13 gotcha 8: never transition `all` on a control with a native background
   * -- it animates background-color during focus and a <select> visibly flickers.
   * Only the border and the ring move. */
  transition: border-color var(--dur) var(--ease-standard),
              box-shadow var(--dur) var(--ease-standard);
}
:root[data-theme="dark"] input, :root[data-theme="dark"] select,
:root[data-theme="dark"] textarea { background: rgb(var(--gray-900)); }

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgb(var(--accent-500) / .18);
}
input::placeholder, textarea::placeholder { color: var(--ink-2); opacity: .8; }

/* The date input's own calendar glyph follows `color-scheme`, which is set on
 * :root -- but Safari needs it on the element too before it will repaint the icon. */
:root[data-theme="dark"] input[type=date],
:root[data-theme="dark"] input[type=time] { color-scheme: dark; }

/* `flex-start`, not `flex-end`. Aligning the BOTTOM of each field looks right only
 * while every field in the row is the same height -- and `field()` appends an
 * optional hint under the control, so on /osoby "Szukaj" carried a hint line and
 * "Data" did not, which pushed the two inputs visibly out of line with each other.
 * Every label in a row here is one short word, so aligning the tops aligns the
 * controls, and a hint just hangs below its own field where it belongs. */
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: flex-start; }
.row > * { flex: 1 1 8rem; }
/* Any depth: people.js wraps its date field in a width-pinning div, so the .field is
 * a grandchild. The row owns the spacing below it, not the last field's margin. */
.row .field { margin-bottom: 0; }
/* A bare control with no label of its own still belongs on the control line. */
.row > .shrink { flex: 0 0 auto; align-self: flex-end; }

.chips { display: flex; gap: .35rem; flex-wrap: wrap; }
.chip {
  min-height: 36px; padding: .3rem .8rem;
  border: 0; border-radius: var(--radius-pill);
  background: rgb(var(--surface)); color: var(--ink-2);
  box-shadow: inset 0 0 0 1px var(--line);
  font: inherit; font-size: var(--text-sm); font-weight: 550;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-standard),
              color var(--dur) var(--ease-standard),
              box-shadow var(--dur) var(--ease-standard);
}
@media (hover: hover) {
  .chip:hover { box-shadow: inset 0 0 0 1px rgb(var(--accent-500) / .40); color: var(--ink); }
}
.chip[aria-pressed=true] {
  background: var(--accent); color: var(--accent-ink);
  box-shadow: inset 0 0 0 1px rgb(var(--gray-950) / .08);
  font-weight: 650;
}

/* ---- surfaces --------------------------------------------------------------
 * Kit §6.2 ships two card systems and is explicit that the split is by ROLE, not
 * taste: content surfaces lift and glow, dashboard tiles stay flat, and a grid where
 * every tile lifts is noise. Almost every card in this app is a row in a long list
 * -- thirty desks, a day of meetings -- so the flat one is the default here. The
 * lift is spent only on cards that are actually actionable, detected with :has()
 * rather than by asking six call sites to pass a modifier class.
 */

.card {
  background: rgb(var(--surface));
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: .85rem;
  margin: 0 0 .6rem;
  box-shadow: var(--shadow-rest);
  transition: border-color var(--dur-slow) var(--ease-spring),
              box-shadow var(--dur-slow) var(--ease-spring),
              transform var(--dur-slow) var(--ease-spring);
}
@media (hover: hover) {
  .card:has(.btn:not(:disabled)):hover {
    transform: translateY(-2px);
    border-color: rgb(var(--accent-500) / .30);
    box-shadow: var(--shadow-lift);
  }
}
/* A day a multi-day booking could not get. Constraint 5 says a half-successful
 * booking is a SUCCESS with a report -- HTTP 200, both lists rendered -- so this sits
 * immediately below a green notice naming the days that did work. The left rule and
 * the tint are what let the two be told apart without reading either heading. */
.card-conflict {
  border-color: rgb(var(--red-500) / .35);
  border-left: 3px solid var(--taken);
  background: var(--taken-soft);
}
.card-conflict .card-title { color: var(--taken); }

.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
.card-title { font-weight: 650; letter-spacing: -.01em; }
.meta { color: var(--ink-2); font-size: var(--text-sm); }
.stack { display: grid; gap: .5rem; }
.grid2 { display: grid; gap: .6rem; }
@media (min-width: 640px) { .grid2 { grid-template-columns: 1fr 1fr; } }

/* A notice is a banner, not a toast: it stays on the screen it belongs to. The
 * `plan.stale` warning (constraint 8) is the one that must never be quiet. */
.notice {
  border-radius: var(--radius-control); padding: .7rem .85rem; margin: 0 0 .75rem;
  border: 1px solid var(--line); background: rgb(var(--surface));
  box-shadow: var(--shadow-rest);
}
.notice strong { display: block; font-weight: 650; }
.notice p { margin: .25rem 0 0; font-size: var(--text-md); color: var(--ink-2); }
.notice-warn { background: var(--warn-soft); border-color: rgb(var(--amber-500) / .40); }
.notice-warn strong { color: var(--warn); }
.notice-error { background: var(--taken-soft); border-color: rgb(var(--red-500) / .40); }
.notice-error strong { color: var(--taken); }
.notice-ok { background: var(--free-soft); border-color: rgb(var(--emerald-500) / .40); }
.notice-ok strong { color: var(--free); }

/* Kit §6.11: one shape for every status pill, and an inset ring instead of a border
 * -- no layout shift, and it composites cleanly over a tinted background. */
.badge {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .15rem .5rem; border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 650; letter-spacing: .01em;
  white-space: nowrap;
}
.badge-free { background: var(--free-soft); color: var(--free); box-shadow: inset 0 0 0 1px rgb(var(--emerald-500) / .30); }
.badge-mine { background: var(--mine-soft); color: var(--mine); box-shadow: inset 0 0 0 1px rgb(var(--blue-500) / .30); }
.badge-taken { background: var(--taken-soft); color: var(--taken); box-shadow: inset 0 0 0 1px rgb(var(--red-500) / .30); }
.badge-fixed { background: var(--fixed-soft); color: var(--fixed); box-shadow: inset 0 0 0 1px rgb(var(--violet-500) / .30); }
.badge-off { background: var(--off-soft); color: var(--off); box-shadow: inset 0 0 0 1px rgb(var(--gray-500) / .28); }
.badge-warn { background: var(--warn-soft); color: var(--warn); box-shadow: inset 0 0 0 1px rgb(var(--amber-500) / .35); }

.loading { display: flex; align-items: center; gap: .6rem; padding: 1.5rem 1rem; color: var(--ink-2); }
.spin {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--line-solid); border-top-color: var(--accent);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- toasts ----------------------------------------------------------------
 * Kit §6.9. The container is always in the document even when empty, and each toast
 * carries a draining progress bar. dom.js removes a non-error toast after 4500ms;
 * errors stay until dismissed, because a booking conflict is something to read --
 * so the bar is suppressed on those rather than draining to a toast that never goes.
 */

#toasts {
  position: fixed; z-index: var(--z-float);
  left: .5rem; right: .5rem; bottom: calc(.5rem + env(safe-area-inset-bottom));
  display: grid; gap: .4rem; pointer-events: none;
}
@media (min-width: 640px) { #toasts { left: auto; width: min(26rem, 90vw); } }

.toast {
  pointer-events: auto;
  position: relative; overflow: hidden;
  display: flex; align-items: flex-start; gap: .5rem;
  padding: .7rem .8rem;
  border-radius: var(--radius-control); box-shadow: var(--shadow-panel);
  background: rgb(var(--surface)); border: 1px solid var(--line);
  font-size: var(--text-md);
  animation: toast-in var(--dur-slow) var(--ease-luxury);
}
.toast::after {
  content: '';
  position: absolute; left: 0; bottom: 0; height: 2px;
  width: 100%;
  background: var(--accent);
  transform-origin: left;
  animation: toast-drain 4.5s linear forwards;
}
.toast-ok { background: var(--free-soft); border-color: rgb(var(--emerald-500) / .40); }
.toast-ok::after { background: var(--free); }
.toast-error { background: var(--taken-soft); border-color: rgb(var(--red-500) / .40); }
.toast-error::after { display: none; }

.toast-x {
  margin-left: auto; border: 0; background: transparent; color: inherit;
  font-size: 1.2rem; line-height: 1; cursor: pointer; padding: 0 .2rem;
  border-radius: var(--radius-control);
  opacity: .6;
  transition: opacity var(--dur-fast) var(--ease-standard);
}
.toast-x:hover { opacity: 1; }

@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes toast-drain { from { transform: scaleX(1); } to { transform: scaleX(0); } }

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

.floor-bar {
  display: flex; gap: .5rem; align-items: center; flex-wrap: wrap;
  padding: .5rem max(.75rem, env(safe-area-inset-left));
}
.floor-bar .summary { color: var(--ink-2); font-size: var(--text-sm); }

.mapwrap {
  position: relative;
  margin: 0 max(.5rem, env(safe-area-inset-left));
  border: 1px solid var(--line); border-radius: var(--radius-card);
  background: rgb(var(--surface));
  box-shadow: var(--shadow-rest);
  overflow: hidden;
  touch-action: none;            /* pinch-zoom is handled in map.js */
}
.mapview {
  position: relative;
  width: 100%;
  aspect-ratio: 2456 / 1530;     /* the plan's viewBox, so no reflow on load */
  overflow: hidden;
}
/* CONSTRAINT 10. No transform here, and deliberately no `will-change`, and no
 * `filter` on this element or on anything inside it. Zoom moves the SVG's viewBox
 * (map.js), so the geometry is re-rendered at each zoom level. Any of those three
 * properties promotes the plan to a compositor layer that is rasterised ONCE at
 * fit-to-width -- roughly 343px across on a phone -- after which every zoom is the
 * GPU stretching that one bitmap and the vector data is never consulted again.
 * Chrome re-rasterises when a gesture settles; Safari often keeps the stale raster
 * for good. `will-change: transform` here is exactly what made a zoomed map blurry. */
.mapview > svg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
}
.mapview.no-plan { display: grid; place-items: center; }

/* Desk hit targets are 15-unit circles on a 2456-wide canvas; on a phone that is
 * a couple of pixels, so the whole group gets a fatter transparent stroke to grab.
 * Applied via CSS rather than by editing the artifact (constraint 2). */
.mapview svg g.desk { cursor: pointer; }
.mapview svg g.desk .desk-seat { stroke-width: 2; transition: fill var(--dur-fast) linear; }
/* --plan-selected, not --accent and not --marker: this ring lands on top of a fill
 * that may be any of the four status colours, so it has to stay legible against red
 * in particular -- and it is drawn on the plan, which stays light in dark mode, so
 * it must not follow the theme either. (--marker, which does, is the /sale "Teraz"
 * line: that one sits on a themed surface.)
 *
 * `!important` is load-bearing, not laziness. map.js tints each desk by injecting a
 * rule keyed on the desk's id -- `#desk-D-01 .desk-seat { fill:…; stroke:… }` -- and
 * an ID selector outranks any selector without one, however many classes it carries.
 * So this rule's colour silently lost to the status tint and a selected desk was
 * only ever drawn THICKER, never in the selection colour. It went unnoticed because
 * the old accent was the same blue as --mine. `!important` is the one thing that
 * beats an ID, and the selected state genuinely does have to win over the status. */
.mapview svg g.desk.rz-selected .desk-seat {
  stroke: var(--plan-selected) !important;
  stroke-width: 5;
}
/* `.rz-live`, not every `g.room`: the plan draws 22 spaces and only 14 of them are
 * bookable rooms (constraint 3). The other eight -- the kitchen, the corridors, the
 * open-plan zones -- are real shapes with real hit areas, so a blanket cursor on
 * `g.room` promised a sheet to anyone hovering the kitchen and then did nothing when
 * they clicked. map.js marks the ones GET /floor actually returned. */
.mapview svg g.room.rz-live { cursor: pointer; }
.mapview svg g.room .room-shape { transition: fill var(--dur-fast) linear; }
.mapview svg .desk-label { user-select: none; }

/* The lift/exit/kitchen badges live in the OUTER artifact, not in the embedded
 * drawing, so unlike walls and floors they are reachable by CSS -- which is exactly
 * why they are absent from plan-dark.js's substitution table. Inverted here instead,
 * or five white discs punch holes through the dark plan while the symbols underneath
 * them have gone dark. Same values as the drawing's own mapping, so the badge and
 * the shape it sits on agree. */
:root[data-theme="dark"] .mapview svg .poi-badge { fill: #11151A; stroke: #7C8794; }
:root[data-theme="dark"] .mapview svg .poi-glyph { stroke: #AEB6C0; }

.mapctl {
  position: absolute; right: .5rem; bottom: .5rem;
  z-index: var(--z-float);
  display: grid; gap: .3rem;
}
.mapctl button {
  width: 38px; height: 38px;
  border: 1px solid var(--line); border-radius: var(--radius-control);
  background: rgb(var(--surface)); color: var(--ink);
  font: inherit; font-size: 1.1rem; cursor: pointer;
  box-shadow: var(--shadow-panel);
  transition: background-color var(--dur) var(--ease-standard),
              color var(--dur) var(--ease-standard),
              transform var(--dur) var(--ease-spring);
}
@media (hover: hover) {
  /* Kit §6.1: icons inside buttons move. It is the tell that makes the set feel
   * like one system. */
  .mapctl button:hover {
    color: var(--accent-strong); border-color: rgb(var(--accent-500) / .40);
    transform: translateY(-1px) scale(1.04);
  }
  :root[data-theme="dark"] .mapctl button:hover { color: var(--accent); }
}

.legend {
  display: flex; gap: .75rem; flex-wrap: wrap;
  padding: .6rem max(.75rem, env(safe-area-inset-left));
  font-size: var(--text-sm); color: var(--ink-2);
}
.legend span { display: inline-flex; align-items: center; gap: .35rem; }
.legend i { width: 12px; height: 12px; border-radius: 50%; border: 1px solid var(--line); }

/* ---- the room day calendar -------------------------------------------------
 * A room per row, an hour per 64 px, a booking per absolutely-positioned block.
 * Sixteen hours do not fit a phone, so the lanes scroll horizontally while the room
 * names stay put -- and the scrollbar is deliberately NOT hidden here: a suppressed
 * scrollbar is how the nav managed to hide half its tabs.
 *
 * The 1/2/3 z-indexes below are local to .cal-scroll and are not part of the global
 * ladder at the top of the file.
 */
.cal-scroll {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: rgb(var(--surface));
  box-shadow: var(--shadow-rest);
}
.cal-grid { display: inline-block; min-width: 100%; }

.cal-head { display: flex; position: sticky; top: 0; z-index: 3; background: rgb(var(--surface)); }
.cal-hours { position: relative; height: 1.6rem; }
.cal-hour {
  position: absolute; top: .3rem;
  transform: translateX(-50%);
  font-size: var(--text-micro); font-weight: 600; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cal-row { display: flex; border-top: 1px solid var(--line); }

/* Sticky so the room being read is always identifiable, however far right you are. */
.cal-name {
  position: sticky; left: 0; z-index: 2;
  flex: 0 0 6.5rem;
  display: flex; align-items: center; justify-content: space-between; gap: .3rem;
  padding: .4rem .5rem;
  border: 0; border-right: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink); font: inherit; font-size: var(--text-xs); font-weight: 600;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-standard);
}
@media (hover: hover) { .cal-name:not(.cal-name--head):hover { background: rgb(var(--accent-500) / .10); } }
.cal-name--head { background: rgb(var(--surface)); cursor: default; }
.cal-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-name small { color: var(--ink-2); flex: 0 0 auto; font-weight: 500; }

.cal-lane {
  position: relative;
  height: 2.4rem;
  /* One line per hour, so a block's extent can be read against the ruler without
   * counting pixels. */
  background-image: repeating-linear-gradient(
    to right, var(--line) 0 1px, transparent 1px 64px);
  cursor: copy;
}

.cal-block {
  position: absolute; top: .25rem; bottom: .25rem;
  display: flex; align-items: center;
  overflow: hidden;
  padding: 0 .4rem;
  border: 1px solid rgb(var(--red-500) / .45);
  border-left: 3px solid var(--taken);
  border-radius: 6px;
  background: var(--taken-soft);
  color: var(--ink);
  font: inherit; font-size: var(--text-xs); font-weight: 550;
  white-space: nowrap;
  cursor: pointer;
  transition: box-shadow var(--dur) var(--ease-standard);
}
@media (hover: hover) { .cal-block:hover { box-shadow: var(--shadow-panel); } }
.cal-block.is-mine {
  border-color: rgb(var(--blue-500) / .45); border-left-color: var(--mine);
  background: var(--mine-soft);
}
.cal-block span { overflow: hidden; text-overflow: ellipsis; }

/* The live marker. Drawn per lane rather than as one tall element so it cannot be
 * clipped by a row's own overflow. On --marker rather than --accent: it is drawn
 * straight across lanes full of pale-red booking blocks. */
.cal-now {
  position: absolute; top: 0; bottom: 0;
  width: 2px;
  background: var(--marker);
  pointer-events: none;
}
/* Deliberately NOT uppercase and not letter-spaced, unlike the other micro-labels in
 * this file. The pill is centred on the current time inside the hour ruler, so every
 * pixel it gains covers another hour number -- "TERAZ" with tracking was wide enough
 * to hide 11:00 entirely. Sentence case keeps it under one hour column. */
.cal-now--label {
  top: .3rem; bottom: auto;
  width: auto; padding: 0 .28rem;
  border-radius: 4px;
  background: var(--marker); color: var(--marker-ink);
  font-size: var(--text-micro); font-weight: 700; line-height: 1.5;
  transform: translateX(-50%);
}

/* ---- sheet (desk / room detail) --------------------------------------------
 * Kit §6.8: max-height plus overflow-y on every modal, or a long form traps its
 * own submit button off-screen. This is the whole booking surface, so it is the
 * component that most has to feel finished.
 */

dialog.sheet {
  border: 0; padding: 0; margin: 0;
  position: fixed; inset: auto 0 0 0;
  width: 100%; max-width: 100%; max-height: 88vh;
  background: rgb(var(--surface)); color: var(--ink);
  border-radius: var(--radius-hero) var(--radius-hero) 0 0;
  box-shadow: var(--shadow-panel);
}
dialog.sheet::backdrop { background: rgb(var(--gray-950) / .55); }
dialog.sheet[open] { animation: sheet-up var(--dur-slow) var(--ease-luxury); }
dialog.sheet[open]::backdrop { animation: fade-in var(--dur) var(--ease-standard); }

/* A grab handle: the sheet is dismissible by tapping the backdrop, and the handle is
 * what says so on a phone where the backdrop is a thin strip. */
dialog.sheet::before {
  content: '';
  position: absolute; top: .5rem; left: 50%;
  width: 2.25rem; height: .25rem;
  transform: translateX(-50%);
  border-radius: var(--radius-pill);
  background: var(--line-solid);
}

@media (min-width: 640px) {
  dialog.sheet {
    inset: 50% auto auto 50%; transform: translate(-50%, -50%);
    width: min(30rem, 92vw); border-radius: var(--radius-card);
  }
  dialog.sheet::before { display: none; }
  dialog.sheet[open] { animation: sheet-zoom var(--dur) var(--ease-luxury); }
}

.sheet-body { padding: 1.15rem 1rem 1rem; overflow-y: auto; max-height: 80vh; }
.sheet-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .75rem; }
.sheet-head h2 { margin: 0; flex: 1; }

@keyframes sheet-up { from { transform: translateY(100%); } }
@keyframes fade-in { from { opacity: 0; } }
/* The desktop variant has to carry the centring transform through the keyframes,
 * or the sheet flies in from the corner. */
@keyframes sheet-zoom {
  from { opacity: 0; transform: translate(-50%, -50%) scale(.94); }
}

/* ---- day picker ----------------------------------------------------------- */

.daygrid { display: grid; grid-template-columns: repeat(7, 1fr); gap: .25rem; }
.daycell {
  display: grid; place-items: center; gap: 0;
  min-height: 46px; padding: .2rem;
  border: 1px solid var(--line); border-radius: var(--radius-control);
  background: rgb(var(--surface)); color: var(--ink);
  font: inherit; font-size: var(--text-xs); cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.daycell b { font-size: var(--text-md); font-weight: 700; font-variant-numeric: tabular-nums; }
.daycell small { font-size: var(--text-micro); color: var(--ink-2); }
.daycell:disabled { cursor: not-allowed; opacity: .5; background: var(--surface-2); }
@media (hover: hover) {
  .daycell:hover:not(:disabled):not([aria-pressed=true]) { border-color: rgb(var(--accent-500) / .45); }
}
.daycell[aria-pressed=true] {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.daycell[aria-pressed=true] small { color: var(--accent-ink); opacity: .85; }
.daycell.is-taken { background: var(--taken-soft); }
.daycell.is-mine { background: var(--mine-soft); }
.daycell.is-weekend b { color: var(--ink-2); }
.dayhead { display: grid; grid-template-columns: repeat(7, 1fr); gap: .25rem; margin-bottom: .25rem; }
.dayhead span {
  text-align: center;
  font-size: var(--text-micro); font-weight: 650; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-2);
}

/* ---- tablet display --------------------------------------------------------
 * The door display is read from across a corridor rather than in the hand, so the
 * state is carried by a large colour field down the side plus oversized type -- not
 * by a dark full-bleed background.
 *
 * Every value here is literal rather than a token, on purpose. This screen is a
 * fixed device with no person and no theme toggle attached to it: it must render
 * identically whatever `data-theme` happens to be in that browser's localStorage,
 * and it must not change appearance because somebody restyled the staff app.
 */
.tablet {
  position: fixed; inset: 0;
  /* Header, hero, then the day rail taking every pixel that is left -- the rail is
   * positioned in percentages, so it needs a definite height to resolve against and
   * `minmax(0, 1fr)` is what keeps that height from being the content's. */
  display: grid; grid-template-rows: auto auto minmax(0, 1fr);
  padding: clamp(1rem, 3vw, 2.5rem);
  gap: clamp(.6rem, 1.5vw, 1.2rem);
  color: #111827;
  background: #ffffff;
  border-left: clamp(10px, 2vw, 22px) solid #059669;
  transition: border-color .5s ease, background .5s ease;
}
/* Offline, revoked and out-of-service all hide the rail, because none of them can
 * honestly claim what the day holds. With it gone the hero takes the slack back
 * instead of stranding itself at the top of an empty screen. */
.tablet.is-norail { grid-template-rows: auto 1fr auto; }
.tablet.is-norail .tablet-main { align-content: center; }
.tablet.is-busy { border-left-color: #dc2626; background: #fffafa; }
.tablet.is-offline { border-left-color: #9ca3af; background: #fafafa; }
/* Room name hard left, clock and the night toggle together on the right. The auto
 * margin rather than space-between, which would strand the clock in the middle of
 * the header once a third item joined the row. */
.tablet-top { display: flex; align-items: baseline; gap: .75rem; }
.tablet-room {
  font-family: 'Fraunces', Georgia, serif;
  font-size: clamp(1.6rem, 5vw, 3.2rem); font-weight: 600; letter-spacing: -.02em;
  margin: 0; color: #111827;
}
.tablet-clock {
  margin-left: auto;
  font-size: clamp(1.4rem, 4vw, 2.6rem); font-variant-numeric: tabular-nums; color: #6b7280;
}
.tablet-main { display: grid; align-content: start; gap: .5rem; }

/* The one word somebody reads from ten metres away.
 *
 * NOT the display face, and that is the whole point. Fraunces carries an `opsz`
 * axis from 9 to 144, `font-optical-sizing` defaults to `auto`, and at this size
 * the browser feeds it ~96pt -- Fraunces' display cut, whose horizontals are
 * deliberate hairlines. Beautiful in a magazine at arm's length; from the far end
 * of a corridor the thin strokes drop out and ZAJĘTA collapses into a row of
 * disconnected slabs. A grotesque at 800 keeps every stroke the same weight, which
 * is the only property that survives distance, glare and an off-axis viewing angle.
 * Positive tracking because the string is all-caps. */
.tablet-state {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: clamp(2.6rem, 11vw, 7rem); font-weight: 800;
  letter-spacing: .005em; line-height: 1;
  color: #047857;
}
.tablet.is-busy .tablet-state { color: #b91c1c; }
.tablet.is-offline .tablet-state { color: #6b7280; }
.tablet-detail { font-size: clamp(1.1rem, 3vw, 2rem); color: #111827; }
.tablet-sub { font-size: clamp(.95rem, 2vw, 1.3rem); color: #6b7280; }

/* ---- instant booking ----
 * Sized for somebody already late for the meeting, reaching out on the way past: a
 * wall panel is tapped without aiming, often at arm's length and off-axis. So the
 * buttons split the full width rather than sitting at their natural size -- the
 * target is as large as the screen can make it, and there is nothing next to it to
 * hit by mistake. */
.tablet-actions { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: .6rem; }
.tablet-btn {
  flex: 1 1 12rem;
  font: inherit; font-weight: 700;
  font-size: clamp(1.15rem, 2.8vw, 1.9rem);
  padding: .85em 1.2em; min-height: 3.2em;
  border: none; border-radius: 999px;
  background: #047857; color: #ffffff;
  cursor: pointer;
  transition: background .15s ease, transform .1s ease;
}
/* Green is the FREE screen's colour, and on the red one it reads as a stray signal
 * -- the room is not free, so nothing on that screen should be saying so. Releasing
 * is not destructive either, so red would be the wrong end of the scale as well:
 * neutral is the one weight that states "this is the action here" without claiming
 * a state the screen contradicts. */
.tablet.is-busy .tablet-btn { background: #111827; }
.tablet-btn:active { transform: translateY(1px); }
.tablet-btn:disabled { background: #9ca3af; cursor: default; transform: none; }
/* Stands where the buttons normally are, for a room that is free but not for long
 * enough to claim. Readable from the corridor like everything else on this screen,
 * and quiet rather than alarming: nothing is broken, there is simply nothing to
 * offer. */
.tablet-note {
  flex: 1 1 100%;
  margin: 0;
  font-size: clamp(1rem, 2.2vw, 1.5rem);
  font-weight: 600;
  color: #6b7280;
  text-align: center;
}
/* A wall display has no hover and no undo, so the outcome is stated in place and
 * left up for a few seconds rather than flashed as a toast nobody is watching.
 * Neutral rather than green for the same reason: a confirmation is not a state, and
 * it is read most often on the busy screen, right after a booking succeeded. */
.tablet-flash { font-size: clamp(.95rem, 2vw, 1.3rem); font-weight: 650; }
.tablet-flash.is-ok { color: #374151; }
.tablet-flash.is-error { color: #b91c1c; }

/* ---- the day rail ----
 * One room, time running downwards, drawn in percentages of the window it covers so
 * it fits whatever height the device gives it and never needs scrolling -- nobody
 * swipes a screen bolted to a wall.
 *
 * It starts at now (or at the meeting already running) and never earlier, because
 * GET /device/room drops meetings that have finished. Drawing the office day from
 * 08:00 would render a blank morning that reads as "nothing was booked", which the
 * payload does not say and the tablet cannot find out. */
.tablet-rail { display: grid; grid-template-rows: auto minmax(0, 1fr); gap: .35rem; min-height: 0; }
.tablet-rail-head {
  font-size: var(--text-sm); font-weight: 650; letter-spacing: .06em;
  text-transform: uppercase; color: #9ca3af;
}
.tablet-rail-body { position: relative; min-height: 0; --rail-gutter: 4.2em; }
.tablet-rail-hour {
  position: absolute; left: 0; width: calc(var(--rail-gutter) - .7em);
  text-align: right; transform: translateY(-.7em);
  font-size: clamp(.8rem, 1.5vw, 1.05rem); font-variant-numeric: tabular-nums;
  color: #9ca3af;
}
.tablet-rail-line {
  position: absolute; left: var(--rail-gutter); right: 0;
  border-top: 1px solid #f3f4f6;
}
/* Free time is the lane itself, so an empty rail is not blank -- it is green. */
.tablet-rail-lane {
  position: absolute; left: var(--rail-gutter); right: 0; top: 0; bottom: 0;
  background: #ecfdf5; border: 1px solid #d1fae5; border-radius: var(--radius-sm);
  overflow: hidden;
}
.tablet-rail-block {
  position: absolute; left: 0; right: 0;
  /* Proportional height, floored so a 15-minute booking on a nine-hour rail is still
   * tall enough to read. Overlaps are impossible (the exclusion constraint), so the
   * only cost is a short block reaching a little past its true end. Kept as low as
   * two lines allow, because every em here is distortion. */
  min-height: 3em;
  padding: .15em .7em; overflow: hidden;
  background: #fee2e2; border-left: 4px solid #dc2626; color: #7f1d1d;
  /* flex-start, NOT center: when the box is shorter than its content, centring
   * clips the title and the times equally and you can read neither. Anchored to the
   * top, the title always survives and the meta line is what gives way. */
  display: flex; flex-direction: column; justify-content: flex-start;
  line-height: 1.15;
}
.tablet-rail-block.is-current { background: #dc2626; border-left-color: #7f1d1d; color: #ffffff; }
.tablet-rail-block b {
  font-size: clamp(.9rem, 1.7vw, 1.3rem); font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tablet-rail-block small {
  font-size: clamp(.75rem, 1.35vw, 1rem); opacity: .85;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* "Now" is drawn twice, and the split is the point.
 *
 * In the gutter it is unconditional -- whatever is booked, the pill is readable.
 * Across the lane it is appended BEFORE the blocks, so a booking paints over it: a
 * hairline through "10:45-11:45 · Anna Kowalska" costs more than it tells you,
 * while over free time it is exactly the "you are here" the rail exists for. */
.tablet-rail-now {
  position: absolute; left: 0; width: var(--rail-gutter);
  border-top: 2px solid #111827;
  pointer-events: none;
}
.tablet-rail-nowline {
  position: absolute; left: 0; right: 0;
  border-top: 2px solid #111827;
  pointer-events: none;
}
.tablet-rail-now span {
  position: absolute; left: 0; top: 0; transform: translateY(-55%);
  font-size: clamp(.7rem, 1.3vw, .95rem); font-weight: 700; letter-spacing: .08em;
  color: #ffffff; background: #111827;
  padding: .1em .5em; border-radius: 999px;
}
.tablet-rail-empty {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: clamp(1rem, 2vw, 1.4rem); color: #047857;
}
.tablet-dot {
  position: fixed; top: .6rem; right: .6rem;
  width: 10px; height: 10px; border-radius: 50%;
  background: #d1d5db;
}
.tablet-dot.stale { background: #dc2626; }
.tablet-setup { max-width: 30rem; margin: 10vh auto; padding: 1rem; }

/* ---- night mode ------------------------------------------------------------
 * A door panel is lit for twelve hours a day, every day, showing largely the same
 * pixels. This exists for the hardware as much as for the eye.
 *
 * Manual, and manual only. Not a clock, not `prefers-color-scheme`, and above all
 * NOT `data-theme`: the staff app's toggle writes to localStorage on this same
 * origin, and a display in a corridor must not go dark because somebody flipped a
 * switch on their phone once. The tablet keeps its own key, so neither can reach
 * the other — the isolation the rest of this block is built on, preserved.
 *
 * Every state colour shifts a step lighter, which is the kit's dark-surface rule:
 * the #b91c1c that reads as a clear red on white reads as brown on near-black.
 * Still all literals, for the reason at the top of this section.
 */
.tablet.is-night { background: #05070a; color: #e5e7eb; border-left-color: #10b981; }
.tablet.is-night.is-busy { background: #100708; border-left-color: #ef4444; }
.tablet.is-night.is-offline { background: #07080a; border-left-color: #4b5563; }
.tablet.is-night .tablet-room { color: #e5e7eb; }
.tablet.is-night .tablet-clock { color: #9ca3af; }
.tablet.is-night .tablet-state { color: #34d399; }
.tablet.is-night.is-busy .tablet-state { color: #f87171; }
.tablet.is-night.is-offline .tablet-state { color: #9ca3af; }
.tablet.is-night .tablet-detail { color: #e5e7eb; }
.tablet.is-night .tablet-sub { color: #9ca3af; }
.tablet.is-night .tablet-note { color: #9ca3af; }
.tablet.is-night .tablet-btn { background: #059669; }
/* The no-green-on-the-red-screen rule holds in the dark too. */
.tablet.is-night.is-busy .tablet-btn { background: #374151; color: #e5e7eb; }
.tablet.is-night .tablet-btn:disabled { background: #374151; color: #9ca3af; }
.tablet.is-night .tablet-flash.is-ok { color: #9ca3af; }
.tablet.is-night .tablet-flash.is-error { color: #f87171; }
.tablet.is-night .tablet-rail-head,
.tablet.is-night .tablet-rail-hour { color: #4b5563; }
.tablet.is-night .tablet-rail-line { border-top-color: #111827; }
.tablet.is-night .tablet-rail-lane { background: #04211a; border-color: #06392c; }
.tablet.is-night .tablet-rail-block {
  background: #3a1113; border-left-color: #ef4444; color: #fecaca;
}
.tablet.is-night .tablet-rail-block.is-current {
  background: #b91c1c; border-left-color: #fca5a5; color: #ffffff;
}
.tablet.is-night .tablet-rail-empty { color: #34d399; }
.tablet.is-night .tablet-rail-now,
.tablet.is-night .tablet-rail-nowline { border-top-color: #e5e7eb; }
.tablet.is-night .tablet-rail-now span { background: #e5e7eb; color: #05070a; }
.tablet.is-night .tablet-dot { background: #374151; }

/* The toggle. Quiet enough that corridor traffic leaves it alone, big enough that
 * somebody who wants it does not have to aim -- it sits in the header rather than
 * floating over the rail, where it would cover a booking. `align-self` because the
 * row is baseline-aligned for the room name and the clock. */
.tablet-night {
  flex: 0 0 auto; align-self: center;
  width: 48px; height: 48px;
  display: inline-grid; place-items: center;
  padding: 0; border: none; border-radius: 999px;
  background: transparent; color: #9ca3af;
  cursor: pointer;
}
.tablet-night:active { background: rgb(0 0 0 / .07); }
.tablet.is-night .tablet-night { color: #6b7280; }
.tablet.is-night .tablet-night:active { background: rgb(255 255 255 / .09); }
/* The icon shows the mode you would GET, matching the aria-label, which names the
 * action. The two glyph tokens are shape-only data-URIs painted by currentColor --
 * the only tokens this screen borrows, and they carry no colour to drift with. */
.tablet-night-icon {
  width: 1.5rem; height: 1.5rem;
  background: currentColor;
  -webkit-mask: var(--glyph-moon) no-repeat center / contain;
  mask: var(--glyph-moon) no-repeat center / contain;
}
.tablet.is-night .tablet-night-icon {
  -webkit-mask-image: var(--glyph-sun);
  mask-image: var(--glyph-sun);
}

/* ---- administration --------------------------------------------------------
 * Four screens behind one header tab. Everything here is composed from components
 * defined above -- cards, badges, chips, notices -- so this section adds only the
 * three things administration needs that no staff screen does: sub-navigation, a
 * type-ahead that resolves to a person, and a way to draw a row as inert.
 *
 * The pattern worth keeping: an admin list is the SAME card as a staff list. A
 * dense table was the tempting alternative and it is the wrong instinct here --
 * these screens get read on a phone as often as a laptop (the realistic use is
 * somebody at reception being asked to unlock an account), and seven columns serve
 * neither. The card already collapses.
 */

/* Sub-navigation. Underlined rather than pill-shaped, so it reads as a level BELOW
 * the header's tabs instead of competing with them: the header owns the accent
 * pill (kit §5.3), and repeating that shape here would put two "you are here"
 * indicators on screen with nothing to say which one is which. */
.subnav {
  display: flex; gap: .1rem; margin: 0 0 1rem;
  overflow-x: auto; scrollbar-width: none;
  border-bottom: 1px solid var(--line);
}
.subnav::-webkit-scrollbar { display: none; }
.subnav-tab {
  flex: 0 0 auto;
  padding: .55rem .8rem;
  color: var(--ink-2); text-decoration: none;
  font-size: var(--text-md); font-weight: 550;
  white-space: nowrap;
  /* Transparent rather than absent: a border that appears only on the current tab
   * would shift every other tab up by two pixels as you move between them. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
@media (hover: hover) {
  .subnav-tab:hover { color: var(--ink); border-bottom-color: var(--line-solid); }
}
.subnav-tab[aria-current="page"] {
  color: var(--accent-strong); font-weight: 650;
  border-bottom-color: var(--accent);
}
:root[data-theme="dark"] .subnav-tab[aria-current="page"] { color: var(--accent); }

/* A row that is still real but no longer live: a deactivated account, a cancelled
 * booking, a revoked token. Dimmed rather than hidden -- "where did it go?" is the
 * question hiding produces, and since nothing here is ever deleted the list has to
 * be able to show the difference between gone and off.
 *
 * Deliberately a tint and a muted title, NOT `opacity` on the whole card: opacity
 * would fade the badge saying WHY it is dim, and that badge is the one thing on an
 * inactive row still worth reading. */
.card-dim { background: var(--off-soft); border-color: var(--line); }
.card-dim .card-title { color: var(--ink-2); }

/* The person type-ahead. Results overlay rather than push, so choosing somebody
 * does not move the button that was under the cursor a moment earlier. */
.picker { position: relative; }
.picker-results {
  position: absolute; z-index: var(--z-sticky); left: 0; right: 0; top: 100%;
  margin-top: .25rem;
  max-height: 16rem; overflow-y: auto;
  background: rgb(var(--surface));
  border: 1px solid var(--line-solid); border-radius: var(--radius-card);
  box-shadow: var(--shadow-lift);
}
.picker-option {
  display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;
  width: 100%; min-height: var(--tap); padding: .5rem .7rem;
  border: 0; background: transparent; color: var(--ink);
  font: inherit; text-align: left; cursor: pointer;
}
@media (hover: hover) {
  .picker-option:hover { background: rgb(var(--accent-500) / .10); }
}
.picker-option:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.picker-picked { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* A credential shown once. Monospace so an O and a 0 stay distinguishable while
 * somebody reads it aloud down a phone line -- which is exactly how a temporary
 * password reaches its owner here. */
.secret {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-md);
  letter-spacing: .02em;
}

.pager {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .5rem .25rem;
}

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

.list { list-style: none; margin: 0; padding: 0; }
.kv { display: grid; grid-template-columns: auto 1fr; gap: .25rem .6rem; font-size: var(--text-md); }
.kv dt { color: var(--ink-2); }
.kv dd { margin: 0; }

/* Kit §6.5: never ship a bare "No results". The six call sites pass a <p class=
 * "empty"> with one sentence, so the rest of the empty state -- the tinted disc, the
 * frame -- is built here rather than by editing all of them. The glyph is a CSS mask
 * over a data-URI, so no markup and no image request. */
.empty {
  display: grid; place-items: center; gap: .6rem;
  padding: 1.75rem 1rem;
  border: 1px dashed var(--line-solid); border-radius: var(--radius-card);
  color: var(--ink-2); font-size: var(--text-md); text-align: center;
}
.empty::before {
  content: '';
  width: 1.75rem; height: 1.75rem;
  /* The glyph is painted by the accent token and cut out by a mask, rather than
   * being an SVG with a colour baked into it -- so it follows a rebrand and the
   * dark palette, and the kit's "no raw accent literal in a component rule" holds. */
  background: rgb(var(--accent-500) / .55);
  -webkit-mask: var(--glyph-calendar) no-repeat center / contain;
  mask: var(--glyph-calendar) no-repeat center / contain;
}

.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- motion ---------------------------------------------------------------
 * Everything above animates only opacity, transform, colour and shadow. There is
 * deliberately no page-level entry animation: the router re-renders on every
 * navigation AND on a visibility-change refresh, so a fade-up would replay each time
 * data came back -- which is the kit's own §9 rule ("entry animations run on first
 * load only") applied to a client that has no swap event to hang it off.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  /* The spinner still has to read as motion, so it is slowed rather than stopped. */
  .spin { animation-duration: 2s !important; }
  /* A toast that is removed after 4.5s must not look full for all of it. */
  .toast::after { animation-duration: 4.5s !important; }
}
