/*
 * plinth-web — the stylesheet.
 *
 * ============================================================================
 * WHY THERE IS NO TAILWIND HERE
 * ============================================================================
 *
 * entropy-ui is built on NativeWind v4, which exists to solve a problem
 * plinth-web does not have: React Native has no cascade, no stylesheet and no
 * `class` attribute, so NativeWind compiles Tailwind utilities into RN style
 * objects and injects them through a JSI shim. Every app that uses it pays for
 * a Tailwind build (PostCSS, a content-glob scan, a JIT pass) and for
 * react-native-css-interop at runtime. On the web target, all of that machinery
 * produces something the browser already does natively and better.
 *
 * Carrying Tailwind into plinth-web would mean every consuming app keeps a Node
 * toolchain, a `tailwind.config.js` with a correctly-realpathed content glob
 * (see curbcut's README for how that goes wrong silently), and a CSS build step
 * in front of `trunk build`. The whole point of a Rust/wasm client is that
 * `cargo` and `trunk` are the toolchain. So: one hand-written stylesheet,
 * shipped with the crate, copied into the bundle by trunk. No Node anywhere in
 * a plinth-web app's build.
 *
 * The cost is real and worth naming: no utility classes means layout lives in
 * component CSS rather than in the call site, so a one-off tweak is a `style`
 * attribute or a new modifier class rather than `className="mt-3"`. In
 * practice that has been a feature — entropy-ui's own components are full of
 * `cn("...thirty utilities...", className)` strings that no reader can diff.
 *
 * ============================================================================
 * TOKENS
 * ============================================================================
 *
 * The custom-property names below are LITERALLY the ones in
 * entropy-ui/styles/globals.css, with the same `R G B` triplet format and the
 * same values. That is deliberate and load-bearing: the Great Migration's
 * stated risk is four design systems drifting, and the cheapest defence is that
 * a designer changing `--primary` changes it in two files that are
 * byte-comparable rather than in two files that have to be reasoned about.
 * When a token changes in entropy-ui, `scripts/check-tokens.sh` fails here.
 *
 * Two additions entropy-ui does not have, both required by things RN could not
 * express on the web anyway:
 *   --focus-ring    an explicit focus colour. entropy-ui's globals.css removes
 *                   the browser focus outline outright ("apps style focus
 *                   themselves") and then no app does. That is a keyboard
 *                   accessibility regression on every web build in the estate
 *                   and plinth-web does not reproduce it.
 *   --plinth-scale  the text-scale preference from curbcut, applied as a real
 *                   root font-size multiplier so that rem-based sizing scales
 *                   without every component reading a signal.
 */

:root {
  --background: 255 255 255;
  --foreground: 0 0 0;

  --card: 255 255 255;
  --card-foreground: 0 0 0;

  --popover: 255 255 255;
  --popover-foreground: 0 0 0;

  --primary: 0 0 0;
  --primary-foreground: 255 255 255;

  --secondary: 245 245 245;
  --secondary-foreground: 0 0 0;

  --muted: 245 245 245;
  --muted-foreground: 115 115 115;

  --accent: 245 245 245;
  --accent-foreground: 0 0 0;

  --destructive: 239 68 68;
  --destructive-foreground: 255 255 255;

  --success: 34 197 94;
  --success-foreground: 255 255 255;
  /* plinth additions, alongside --focus-ring: status tones for warning and
   * info (badges, alerts, toasts, log lines), and the raised surface cards,
   * dialogs, menus and toasts sit on — a token so system dark gets it too. */
  --warning: 180 83 9;
  --info: 29 78 216;
  --surface-raised: 255 255 255;

  --border: 229 229 229;
  --input: 229 229 229;
  --ring: 0 0 0;

  --radius: 0.5rem;

  /* Additions. See the header comment. */
  --focus-ring: 37 99 235;
  --plinth-scale: 1;

  --font-sans: "Geist", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", Monaco, "Cascadia Mono",
    "Fira Code", "Droid Sans Mono", "Courier New", monospace;

  color-scheme: light;
}

/*
 * Dark theme.
 *
 * Three selectors, in this order, and the order matters. `:root[data-theme]`
 * is the explicit user choice and must beat `prefers-color-scheme`; the media
 * query is guarded against `[data-theme="light"]` so that a user who has
 * chosen light on a dark-mode OS actually gets light. entropy-ui toggles a
 * `.dark` class on documentElement from ThemeContext; plinth-web uses a
 * `data-theme` attribute instead, because an attribute has one value where a
 * class can be both present and absent in ways nothing asserts.
 *
 * The `.dark` class selector is kept alongside so that an app mid-migration —
 * one Leptos island inside an RNW page, which is how cloud gets migrated — is
 * not half light and half dark for the duration.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --background: 0 0 0;
    --foreground: 255 255 255;
    --card: 0 0 0;
    --card-foreground: 255 255 255;
    --popover: 0 0 0;
    --popover-foreground: 255 255 255;
    --primary: 255 255 255;
    --primary-foreground: 0 0 0;
    --secondary: 38 38 38;
    --secondary-foreground: 255 255 255;
    --muted: 38 38 38;
    --muted-foreground: 163 163 163;
    --accent: 38 38 38;
    --accent-foreground: 255 255 255;
    --destructive: 127 29 29;
    --destructive-foreground: 255 255 255;
    --border: 38 38 38;
    --input: 38 38 38;
    --ring: 255 255 255;
    --focus-ring: 96 165 250;
  --success: 74 222 128;
  --warning: 252 211 77;
  --info: 147 197 253;
  --surface-raised: 23 23 23;
    --success: 74 222 128;
    --warning: 252 211 77;
    --info: 147 197 253;
    --surface-raised: 23 23 23;
    color-scheme: dark;
  }
}

:root[data-theme="dark"],
.dark {
  --background: 0 0 0;
  --foreground: 255 255 255;
  --card: 0 0 0;
  --card-foreground: 255 255 255;
  --popover: 0 0 0;
  --popover-foreground: 255 255 255;
  --primary: 255 255 255;
  --primary-foreground: 0 0 0;
  --secondary: 38 38 38;
  --secondary-foreground: 255 255 255;
  --muted: 38 38 38;
  --muted-foreground: 163 163 163;
  --accent: 38 38 38;
  --accent-foreground: 255 255 255;
  --destructive: 127 29 29;
  --destructive-foreground: 255 255 255;
  --border: 38 38 38;
  --input: 38 38 38;
  --ring: 255 255 255;
  --focus-ring: 96 165 250;
  color-scheme: dark;
  --success: 74 222 128;
  --warning: 252 211 77;
  --info: 147 197 253;
  --surface-raised: 23 23 23;
}

/*
 * High contrast.
 *
 * curbcut records `highContrast` as a preference and explicitly declines to
 * apply it — "mapping them onto a palette needs the app's colours, so the app
 * has to do it". On web the design system IS the app's colours, so plinth-web
 * applies it here rather than handing every consumer the same unanswered
 * question. What it does: pure black/white poles, borders that are foreground
 * rather than a grey, and muted text pulled up to the contrast of body text
 * (which is the single change that most affects readability, and is why the
 * muted token stops being muted rather than being tuned).
 *
 * It composes with the theme rather than replacing it, so a user can be in
 * dark + high-contrast, which is the common pairing for low-vision users on
 * OLED.
 */
:root[data-contrast="high"] {
  --border: 0 0 0;
  --input: 0 0 0;
  --muted-foreground: 0 0 0;
  --secondary: 255 255 255;
  --accent: 255 255 255;
  --destructive: 176 0 0;
  --focus-ring: 0 0 0;
}

:root[data-contrast="high"][data-theme="dark"],
:root[data-contrast="high"]:not([data-theme="light"]) {
  --border: 255 255 255;
  --input: 255 255 255;
  --muted-foreground: 255 255 255;
  --secondary: 0 0 0;
  --accent: 0 0 0;
  --destructive: 255 130 130;
  --focus-ring: 255 255 255;
}

/* ==========================================================================
   RESET
   Small on purpose. A full normalize.css is mostly corrections for browsers
   the estate does not support.
   ========================================================================== */

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

html {
  /*
   * The text-scale preference lands here, once, rather than in every
   * component. 100% of the browser's own base size — never a px value — so a
   * user who has set a larger default font in their browser keeps it and the
   * app preference multiplies on top of it rather than overriding it.
   */
  font-size: calc(100% * var(--plinth-scale));
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  padding: 0;
  /*
   * Padded by the safe-area insets, so a notched phone in landscape does not
   * put the first control under the cutout.
   */
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background-color: rgb(var(--background));
  color: rgb(var(--foreground));
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img,
svg,
video {
  max-width: 100%;
}

[hidden] {
  display: none !important;
}

/* ==========================================================================
   FOCUS

   The single most consequential block in this file.

   entropy-ui's globals.css contains:

       input:focus, textarea:focus, button:focus,
       [role="button"]:focus, [tabindex]:focus { outline: none; }

   with the comment "apps style focus themselves". No app in the estate does.
   The result is that every react-native-web build ships with no visible focus
   indicator at all — WCAG 2.4.7 failed outright, and the product unusable by
   keyboard for anyone who cannot track an invisible caret. plinth-web does the
   opposite by default and makes removing it the thing you have to write.

   `:focus-visible`, not `:focus`, so a mouse user clicking a button does not
   get a ring while a keyboard user tabbing to the same button does. The ring
   is drawn with `outline` plus `outline-offset` rather than `box-shadow`
   because outline follows the element's shape through border-radius and is not
   clipped by an ancestor's `overflow: hidden` — which is exactly the case for
   a focused row inside a scrolling table.
   ========================================================================== */

:focus-visible {
  outline: 2px solid rgb(var(--focus-ring));
  outline-offset: 2px;
  border-radius: calc(var(--radius) - 4px);
}

/* Never suppress the ring wholesale. Where a component draws its own (the pin
   pad's key, the segmented toggle's thumb) it opts out here, one selector at a
   time, and replaces it with something at least as visible. */
.plinth-focus-custom:focus-visible {
  outline: none;
}

/*
 * Skip link. Present in the stylesheet rather than left to each app because a
 * skip link that only some pages have is worse than none — a keyboard user
 * learns it is not there and stops reaching for it.
 */
.plinth-skip-link {
  position: absolute;
  left: 0.5rem;
  top: -4rem;
  z-index: 1000;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  background: rgb(var(--primary));
  color: rgb(var(--primary-foreground));
  font-weight: 500;
  text-decoration: none;
  transition: top 120ms ease-out;
}

.plinth-skip-link:focus {
  top: calc(0.5rem + env(safe-area-inset-top, 0px));
}

/*
 * The screen-reader-only pattern. `clip-path` + 1px box rather than
 * `display:none` (which removes it from the accessibility tree, defeating the
 * purpose) and rather than a negative text-indent (which some screen readers
 * skip and which breaks in RTL).
 */
.plinth-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   MOTION

   Two gates, and both are needed.

   `prefers-reduced-motion` is the OS-level setting. `[data-motion="reduced"]`
   is curbcut's in-app preference, which exists because the OS setting is
   all-or-nothing across every app and a user who wants motion in their photo
   viewer and not in their password manager has no other way to say so.

   Duration goes to 0.01ms rather than 0: a zero-duration transition never
   fires `transitionend`, and components that wait for it to unmount a closing
   dialog would hang open forever.
   ========================================================================== */

@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;
  }
}

:root[data-motion="reduced"] *,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  scroll-behavior: auto !important;
}

/* ==========================================================================
   TEXT
   ========================================================================== */

.plinth-text {
  margin: 0;
  font-size: 1rem;
  line-height: 1.5;
  color: rgb(var(--foreground));
}

.plinth-text--xs { font-size: 0.75rem; line-height: 1rem; }
.plinth-text--sm { font-size: 0.875rem; line-height: 1.25rem; }
.plinth-text--base { font-size: 1rem; line-height: 1.5rem; }
.plinth-text--lg { font-size: 1.125rem; line-height: 1.75rem; }
.plinth-text--xl { font-size: 1.25rem; line-height: 1.75rem; }
.plinth-text--2xl { font-size: 1.5rem; line-height: 2rem; }
.plinth-text--3xl { font-size: 1.875rem; line-height: 2.25rem; }
.plinth-text--4xl { font-size: 2.25rem; line-height: 2.5rem; }

.plinth-text--muted { color: rgb(var(--muted-foreground)); }
.plinth-text--error { color: rgb(var(--destructive)); font-size: 0.875rem; }
.plinth-text--mono { font-family: var(--font-mono); }
.plinth-text--medium { font-weight: 500; }
.plinth-text--semibold { font-weight: 600; }
.plinth-text--bold { font-weight: 700; }
.plinth-text--center { text-align: center; }

.plinth-heading {
  margin: 0;
  font-weight: 700;
  color: rgb(var(--foreground));
  /*
   * `text-wrap: balance` on headings only. It is O(n²) in line count, so it is
   * genuinely bad on body copy and genuinely good on the two- or three-line
   * heading it was designed for.
   */
  text-wrap: balance;
}

/* ==========================================================================
   LAYOUT PRIMITIVES
   The three that earn their keep. Everything else is a component's own CSS.
   ========================================================================== */

.plinth-stack {
  display: flex;
  flex-direction: column;
  gap: var(--plinth-gap, 1rem);
  min-width: 0;
}

.plinth-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--plinth-gap, 0.75rem);
  min-width: 0;
  /*
   * Wrapping by default. A row that cannot wrap is the single most common
   * cause of a horizontal scrollbar at 400px, and a row of controls that
   * stacks when it must is almost always what was wanted.
   */
  flex-wrap: wrap;
}

.plinth-row--nowrap { flex-wrap: nowrap; }
.plinth-row--between { justify-content: space-between; }
.plinth-row--end { justify-content: flex-end; }
.plinth-row--center { justify-content: center; }
.plinth-row--top { align-items: flex-start; }

.plinth-spacer { flex: 1 1 auto; }

/*
 * Page shell. `min-height: 100dvh` — dynamic viewport height — not `100vh`:
 * on mobile Safari `100vh` is the height with the URL bar hidden, so a page
 * sized to it is always a bar's worth too tall and the footer sits below the
 * fold until the user scrolls.
 */
.plinth-page {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: rgb(var(--background));
}

.plinth-page__main {
  flex: 1 1 auto;
  width: 100%;
  max-width: var(--plinth-page-width, 72rem);
  margin: 0 auto;
  /* The 16px side gutter that must survive every width. Set once, here. */
  padding: 1.5rem 1rem;
}

@media (min-width: 640px) {
  .plinth-page__main { padding: 2rem 1.5rem; }
}

/* ==========================================================================
   BUTTON
   ========================================================================== */

.plinth-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-family: inherit;
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  /*
   * A press dips and dims; hover lifts. entropy-ui does this with Reanimated
   * shared values and a worklet on the UI thread — two animated styles, a
   * hover signal and a press signal per button. Here it is one CSS transform
   * on the compositor, which is the same effect for none of the JS.
   */
  transition:
    transform 120ms ease-out,
    opacity 120ms ease-out,
    background-color 120ms ease-out,
    border-color 120ms ease-out;
  -webkit-tap-highlight-color: transparent;
}

.plinth-btn--sm { min-height: 2.5rem; padding: 0 1rem; font-size: 0.875rem; }
.plinth-btn--md { min-height: 3rem; padding: 0 1rem; font-size: 1rem; }
.plinth-btn--lg { min-height: 3.5rem; padding: 0 1.5rem; font-size: 1.125rem; }

.plinth-btn--default {
  background: rgb(var(--primary));
  color: rgb(var(--primary-foreground));
}

.plinth-btn--destructive {
  background: rgb(var(--destructive));
  color: rgb(var(--destructive-foreground));
}

.plinth-btn--outline {
  background: transparent;
  border-color: rgb(var(--border));
  color: rgb(var(--foreground));
}

.plinth-btn--ghost {
  background: transparent;
  color: rgb(var(--foreground));
}

/*
 * Hover is gated on `(hover: hover)`. Without the gate, a touch device applies
 * :hover on tap and leaves it applied until the next tap elsewhere — the
 * "sticky hover" bug, which on a button that lifts 2.5% means a phone's last
 * tapped button stays visibly raised.
 */
@media (hover: hover) {
  .plinth-btn:hover:not(:disabled) { transform: scale(1.025); }
  .plinth-btn--outline:hover:not(:disabled),
  .plinth-btn--ghost:hover:not(:disabled) { background: rgb(var(--accent)); }
}

.plinth-btn:active:not(:disabled) {
  transform: scale(0.94);
  opacity: 0.92;
}

.plinth-btn:disabled,
.plinth-btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.plinth-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: rgb(var(--foreground));
  border-radius: var(--radius);
  cursor: pointer;
  padding: 0;
  transition: transform 120ms ease-out, background-color 120ms ease-out;
  -webkit-tap-highlight-color: transparent;
}

/*
 * 2.75rem = 44px, the WCAG 2.5.8 / Apple HIG minimum target. entropy-ui's
 * IconButton uses 40/48/56px; the small size is raised to 44 here rather than
 * reproduced at 40, because 40px is below the threshold and the reason RN used
 * it (density on a phone list) applies equally to the web build that was
 * failing the check.
 */
.plinth-icon-btn--sm { width: 2.75rem; height: 2.75rem; }
.plinth-icon-btn--md { width: 3rem; height: 3rem; }
.plinth-icon-btn--lg { width: 3.5rem; height: 3.5rem; }

@media (hover: hover) {
  .plinth-icon-btn:hover:not(:disabled) {
    background: rgb(var(--accent));
    transform: scale(1.06);
  }
}

.plinth-icon-btn:active:not(:disabled) { transform: scale(0.92); }
.plinth-icon-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ==========================================================================
   SPINNER
   ========================================================================== */

.plinth-spinner {
  display: inline-block;
  width: 1.125em;
  height: 1.125em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: plinth-spin 0.7s linear infinite;
}

@keyframes plinth-spin {
  to { transform: rotate(360deg); }
}

/*
 * With motion reduced, a spinner that does not spin communicates nothing. It
 * becomes a pulse instead of vanishing — still a signal that work is in
 * flight, without the rotation that triggers vestibular symptoms.
 */
:root[data-motion="reduced"] .plinth-spinner {
  animation: plinth-pulse 1.4s ease-in-out infinite !important;
  border-right-color: currentColor;
  opacity: 0.6;
}

@media (prefers-reduced-motion: reduce) {
  .plinth-spinner {
    animation: plinth-pulse 1.4s ease-in-out infinite !important;
    border-right-color: currentColor;
    opacity: 0.6;
  }
}

@keyframes plinth-pulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

/* ==========================================================================
   CARD
   ========================================================================== */

.plinth-card {
  border: 1px solid rgb(var(--border));
  border-radius: var(--radius);
  background: rgb(var(--card));
  color: rgb(var(--card-foreground));
  box-shadow: 0 1px 2px rgb(var(--foreground) / 0.06);
}

/*
 * In dark mode the card is lifted off the background rather than left at
 * `--card` (which is pure black, identical to `--background`). entropy-ui hit
 * this and fixed it the same way — see the comment in its card.tsx dated
 * 2026-08-17, "the contrast is not great". Reproducing the fix rather than the
 * bug.
 */
:root[data-theme="dark"] .plinth-card,
.dark .plinth-card {
  background: rgb(var(--surface-raised));
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .plinth-card { background: rgb(var(--surface-raised)); }
}

.plinth-card__header { padding: 1.5rem 1.5rem 1rem; }
.plinth-card__content { padding: 0 1.5rem 1.5rem; }
.plinth-card__footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  padding: 0.5rem 1.5rem 1.5rem;
  border-top: 1px solid rgb(var(--border));
  margin-top: 0.5rem;
}
.plinth-card__title { font-size: 1.25rem; font-weight: 600; margin: 0; }
.plinth-card__description {
  font-size: 0.875rem;
  color: rgb(var(--muted-foreground));
  margin: 0.5rem 0 0;
}

/* ==========================================================================
   FIELD / INPUT
   ========================================================================== */

.plinth-field { display: flex; flex-direction: column; width: 100%; }

.plinth-field__label {
  font-size: 0.875rem;
  font-weight: 500;
  color: rgb(var(--foreground));
  margin-bottom: 0.5rem;
}

.plinth-field__required {
  color: rgb(var(--destructive));
  margin-inline-start: 0.125rem;
}

.plinth-field__hint,
.plinth-field__error {
  font-size: 0.875rem;
  margin-top: 0.5rem;
}

.plinth-field__hint { color: rgb(var(--muted-foreground)); }
.plinth-field__error { color: rgb(var(--destructive)); font-weight: 500; }

.plinth-input-shell {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 3rem;
  padding: 0 0.75rem;
  border: 1px solid rgb(var(--input));
  border-radius: var(--radius);
  background: rgb(var(--card));
  transition: border-color 120ms ease-out, box-shadow 120ms ease-out;
}

/*
 * The shell carries the focus ring, not the bare <input> inside it, because
 * the visible control is the shell — a ring drawn around the input alone sits
 * inside the border and reads as a rendering glitch. `:focus-within` is what
 * makes that possible, and it is also why the inner input's own outline is
 * suppressed here and only here.
 */
.plinth-input-shell:focus-within {
  outline: 2px solid rgb(var(--focus-ring));
  outline-offset: 1px;
  border-color: rgb(var(--foreground));
}

.plinth-input-shell--invalid {
  border-width: 2px;
  border-color: rgb(var(--destructive));
}

/*
 * The dark destructive token (rgb(127 29 29)) is a fill colour and has too
 * little contrast as a 2px outline on black. entropy-ui's input.tsx makes
 * exactly this exception with exactly this reasoning; red-400 is the estate's
 * convention for destructive strokes on dark.
 */
:root[data-theme="dark"] .plinth-input-shell--invalid,
.dark .plinth-input-shell--invalid { border-color: rgb(248 113 113); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .plinth-input-shell--invalid {
    border-color: rgb(248 113 113);
  }
}

.plinth-input-shell--disabled { opacity: 0.6; }

.plinth-input {
  flex: 1 1 auto;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  color: rgb(var(--foreground));
  font-family: inherit;
  /*
   * 16px minimum, always. Mobile Safari zooms the viewport when a focused
   * input's font-size is below 16px, and the zoom is not undone on blur — the
   * user is left on a page they now have to pinch back out of.
   */
  font-size: max(1rem, 16px);
  line-height: 1.5;
  padding: 0.625rem 0;
  width: 100%;
}

.plinth-input::placeholder { color: rgb(var(--muted-foreground)); opacity: 0.8; }

.plinth-input--mono { font-family: var(--font-mono); letter-spacing: 0.02em; }

.plinth-textarea {
  min-height: 6.25rem;
  padding: 0.75rem;
  resize: vertical;
  line-height: 1.5;
}

.plinth-input-adornment {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: rgb(var(--muted-foreground));
  flex: 0 0 auto;
}

.plinth-input-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px target even though the glyph is 20px. */
  min-width: 2.75rem;
  min-height: 2.75rem;
  margin-inline-end: -0.5rem;
  border: none;
  background: transparent;
  color: rgb(var(--muted-foreground));
  cursor: pointer;
  border-radius: var(--radius);
}

@media (hover: hover) {
  .plinth-input-toggle:hover { color: rgb(var(--foreground)); }
}

/* ==========================================================================
   CHECKBOX / SWITCH
   ========================================================================== */

.plinth-checkbox {
  display: inline-flex;
  align-items: flex-start;
  gap: 0.625rem;
  cursor: pointer;
  /* The whole label is the target, not just the 20px box. */
  min-height: 2.75rem;
  padding-block: 0.375rem;
}

.plinth-checkbox__box {
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 0.125rem;
  border: 1px solid rgb(var(--input));
  border-radius: calc(var(--radius) - 4px);
  background: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: rgb(var(--primary-foreground));
  transition: background-color 120ms ease-out, border-color 120ms ease-out;
}

.plinth-checkbox input:checked + .plinth-checkbox__box {
  background: rgb(var(--primary));
  border-color: rgb(var(--primary));
}

/*
 * The real <input type="checkbox"> is visually hidden but present and focused
 * — the box is decoration. This is the single most important accessibility
 * decision in the file: a div with role="checkbox" (which is what
 * @rn-primitives/checkbox compiles to under react-native-web) does not
 * participate in form submission, does not respond to the space key without
 * hand-written handling, and is not announced by Dragon or by switch control
 * the way the native element is.
 */
.plinth-checkbox__input {
  position: absolute;
  opacity: 0;
  width: 1.25rem;
  height: 1.25rem;
  margin: 0;
}

.plinth-checkbox__input:focus-visible + .plinth-checkbox__box {
  outline: 2px solid rgb(var(--focus-ring));
  outline-offset: 2px;
}

.plinth-checkbox__label { font-size: 0.875rem; line-height: 1.4; }

/* ==========================================================================
   SEPARATOR
   ========================================================================== */

.plinth-separator { background: rgb(var(--border)); border: 0; margin: 0; }
.plinth-separator--horizontal { height: 1px; width: 100%; }
.plinth-separator--vertical { width: 1px; align-self: stretch; }

.plinth-separator-text {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
}

.plinth-separator-text::before,
.plinth-separator-text::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: rgb(var(--border));
}

.plinth-separator-text > span {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgb(var(--muted-foreground));
}

/* ==========================================================================
   SKELETON
   ========================================================================== */

.plinth-skeleton {
  border-radius: calc(var(--radius) - 2px);
  background: rgb(var(--muted));
  animation: plinth-skeleton 1.5s ease-in-out infinite;
}

@keyframes plinth-skeleton {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

/* ==========================================================================
   BADGE / TAG CHIP
   ========================================================================== */

.plinth-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.125rem 0.5rem;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.25rem;
  white-space: nowrap;
}

.plinth-badge--neutral {
  background: rgb(var(--secondary));
  color: rgb(var(--secondary-foreground));
  border-color: rgb(var(--border));
}
.plinth-badge--success { background: rgb(var(--success) / 0.15); color: rgb(var(--success)); border-color: rgb(var(--success) / 0.35); }
.plinth-badge--danger { background: rgb(var(--destructive) / 0.15); color: rgb(var(--destructive)); border-color: rgb(var(--destructive) / 0.35); }
.plinth-badge--warning { background: rgb(var(--warning) / 0.15); color: rgb(var(--warning)); border-color: rgb(var(--warning) / 0.35); }
.plinth-badge--info { background: rgb(var(--info) / 0.15); color: rgb(var(--info)); border-color: rgb(var(--info) / 0.35); }

/* (dark warning/info colours come from the tokens) */
/* (dark warning/info colours come from the tokens) */
:root[data-theme="dark"] .plinth-badge--danger,
.dark .plinth-badge--danger { color: rgb(248 113 113); }

.plinth-badge__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* ==========================================================================
   STAT CARD
   ========================================================================== */

.plinth-stat { display: flex; flex-direction: column; gap: 0.25rem; }
.plinth-stat__label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgb(var(--muted-foreground));
}
.plinth-stat__value {
  font-size: 1.875rem;
  font-weight: 700;
  line-height: 1.1;
  /* Tabular figures so a counting value does not shuffle its own width. */
  font-variant-numeric: tabular-nums;
}
.plinth-stat__trend { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.875rem; }
.plinth-stat__trend--up { color: rgb(var(--success)); }
.plinth-stat__trend--down { color: rgb(var(--destructive)); }
.plinth-stat__trend--flat { color: rgb(var(--muted-foreground)); }

/* ==========================================================================
   SEGMENTED TOGGLE
   ========================================================================== */

.plinth-segmented {
  display: inline-flex;
  padding: 0.25rem;
  gap: 0.25rem;
  border-radius: var(--radius);
  background: rgb(var(--secondary));
  border: 1px solid rgb(var(--border));
  /* Scrolls rather than overflows when there are more segments than width. */
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}

.plinth-segmented::-webkit-scrollbar { display: none; }

.plinth-segmented__option {
  flex: 0 0 auto;
  min-height: 2.25rem;
  padding: 0 0.875rem;
  border: none;
  border-radius: calc(var(--radius) - 2px);
  background: transparent;
  color: rgb(var(--muted-foreground));
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 120ms ease-out, color 120ms ease-out;
}

.plinth-segmented__option[aria-selected="true"],
.plinth-segmented__option[aria-checked="true"] {
  background: rgb(var(--background));
  color: rgb(var(--foreground));
  box-shadow: 0 1px 2px rgb(var(--foreground) / 0.1);
}

/* ==========================================================================
   SETTINGS ROW
   ========================================================================== */

.plinth-settings-section {
  display: flex;
  flex-direction: column;
  border: 1px solid rgb(var(--border));
  border-radius: var(--radius);
  overflow: hidden;
  background: rgb(var(--card));
}

.plinth-settings-section__title {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgb(var(--muted-foreground));
  margin: 0 0 0.5rem;
}

.plinth-settings-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  width: 100%;
  min-height: 3.5rem;
  padding: 0.75rem 1rem;
  border: 0;
  border-bottom: 1px solid rgb(var(--border));
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: start;
}

.plinth-settings-row:last-child { border-bottom: 0; }

button.plinth-settings-row,
a.plinth-settings-row { cursor: pointer; }

@media (hover: hover) {
  button.plinth-settings-row:hover,
  a.plinth-settings-row:hover { background: rgb(var(--accent)); }
}

.plinth-settings-row__body { flex: 1 1 auto; min-width: 0; }
.plinth-settings-row__title { font-size: 0.9375rem; font-weight: 500; }
.plinth-settings-row__subtitle {
  font-size: 0.8125rem;
  color: rgb(var(--muted-foreground));
  margin-top: 0.125rem;
}

/* Below 400px a settings row with a long title and a control stacks rather
   than squeezing the title to two characters a line. */
@media (max-width: 26rem) {
  .plinth-settings-row--stack {
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
  }
}

/* ==========================================================================
   TABLE

   A real <table>. Not a virtualised list of flex rows, which is what
   entropy-ui's DataTable is (a FlatList of Views) and what every RNW build in
   the estate therefore ships. The difference matters for more than purity: a
   screen reader announces "column 3 of 7, Status" only for a real table, and
   find-in-page, column sorting by the browser's own UI, and copy-paste into a
   spreadsheet all work only on the real element.

   Virtualisation is still available — see `Table`'s `window` prop — and when
   it is on, the rows outside the window are omitted with `aria-rowcount` set
   so assistive technology is told the real total rather than the rendered one.
   ========================================================================== */

.plinth-table-wrap {
  width: 100%;
  overflow-x: auto;
  border: 1px solid rgb(var(--border));
  border-radius: var(--radius);
  background: rgb(var(--card));
  /* Momentum scrolling and a visible scrollbar on macOS trackpads. */
  -webkit-overflow-scrolling: touch;
}

.plinth-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.plinth-table th,
.plinth-table td {
  padding: 0.75rem;
  text-align: start;
  vertical-align: middle;
}

.plinth-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: rgb(var(--secondary));
  color: rgb(var(--muted-foreground));
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid rgb(var(--border));
  white-space: nowrap;
}

/* Resizable columns: a handle on each header's right edge. */
.plinth-table--resizable th { overflow: hidden; text-overflow: ellipsis; }
.plinth-table--resizable td { overflow: hidden; text-overflow: ellipsis; }
.plinth-table__resize {
  position: absolute; top: 0; right: 0; bottom: 0; width: 0.5rem;
  cursor: col-resize; touch-action: none; z-index: 2;
}
.plinth-table__resize::after {
  content: ""; position: absolute; top: 25%; bottom: 25%; right: 0.1875rem; width: 1px;
  background: rgb(var(--border));
}
@media (hover: hover) {
  .plinth-table__resize:hover::after { background: rgb(var(--foreground) / 0.5); top: 0; bottom: 0; width: 2px; }
}
.plinth-table__resize:focus-visible { outline: none; }
.plinth-table__resize:focus-visible::after { background: rgb(var(--focus-ring)); top: 0; bottom: 0; width: 2px; }

/* Column picker. */
.plinth-colpicker { position: relative; display: inline-block; }
.plinth-colpicker__button {
  height: 2.5rem; padding: 0 0.875rem; border: 1px solid rgb(var(--border)); border-radius: var(--radius);
  background: rgb(var(--background)); color: inherit; font: inherit; font-size: 0.875rem; cursor: pointer; white-space: nowrap;
}
@media (hover: hover) { .plinth-colpicker__button:hover { background: rgb(var(--accent)); } }
.plinth-colpicker__button:focus-visible { outline: 2px solid rgb(var(--focus-ring)); outline-offset: 2px; }
.plinth-colpicker__panel {
  position: absolute; right: 0; top: calc(100% + 0.375rem); z-index: 30; min-width: 12rem;
  display: flex; flex-direction: column; gap: 0.125rem; padding: 0.5rem;
  border: 1px solid rgb(var(--border)); border-radius: var(--radius); background: rgb(var(--popover));
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
}
.plinth-colpicker__item {
  display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.5rem; border-radius: calc(var(--radius) - 2px);
  font-size: 0.875rem; cursor: pointer;
}
@media (hover: hover) { .plinth-colpicker__item:hover { background: rgb(var(--accent)); } }
.plinth-colpicker__item.is-fixed { opacity: 0.6; cursor: default; }
.plinth-colpicker__item input { accent-color: rgb(var(--foreground)); }
.plinth-colpicker__reset {
  margin-top: 0.25rem; padding: 0.375rem 0.5rem; border: 0; border-top: 1px solid rgb(var(--border));
  background: none; color: rgb(var(--muted-foreground)); font: inherit; font-size: 0.8125rem; text-align: left; cursor: pointer;
}

.plinth-table tbody tr { border-bottom: 1px solid rgb(var(--border)); }
.plinth-table tbody tr:last-child { border-bottom: 0; }

.plinth-table tbody tr[data-interactive="true"] { cursor: pointer; }

@media (hover: hover) {
  .plinth-table tbody tr[data-interactive="true"]:hover { background: rgb(var(--accent)); }
}

.plinth-table tbody tr:focus-visible {
  outline: 2px solid rgb(var(--focus-ring));
  outline-offset: -2px;
}

.plinth-table__num { text-align: end; font-variant-numeric: tabular-nums; }
.plinth-table__center { text-align: center; }

.plinth-table__sort {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  padding: 0;
}

.plinth-table__sort-glyph { opacity: 0.4; }
.plinth-table__sort[aria-sort="ascending"] .plinth-table__sort-glyph,
.plinth-table__sort[aria-sort="descending"] .plinth-table__sort-glyph { opacity: 1; }

.plinth-table__empty {
  padding: 3rem 1rem;
  text-align: center;
  color: rgb(var(--muted-foreground));
}

/*
 * Below 40rem a wide table becomes a stack of labelled rows. The `data-label`
 * on each cell carries the header text, which is why `Table` renders it even
 * on desktop where nothing reads it — a CSS-only responsive table cannot
 * invent the label at breakpoint time.
 */
@media (max-width: 40rem) {
  .plinth-table--stack thead { display: none; }
  .plinth-table--stack tbody tr {
    display: block;
    padding: 0.5rem 0;
  }
  .plinth-table--stack tbody td {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.375rem 0.75rem;
    text-align: end;
  }
  .plinth-table--stack tbody td::before {
    content: attr(data-label);
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: rgb(var(--muted-foreground));
    text-align: start;
  }
  .plinth-table--stack tbody td:empty { display: none; }
}

/* ==========================================================================
   PAGINATION
   ========================================================================== */

.plinth-pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 0.75rem 0;
}

.plinth-pagination__pages { display: flex; align-items: center; gap: 0.25rem; flex-wrap: wrap; }

.plinth-pagination__page {
  min-width: 2.5rem;
  min-height: 2.5rem;
  padding: 0 0.5rem;
  border: 1px solid transparent;
  border-radius: calc(var(--radius) - 2px);
  background: transparent;
  color: rgb(var(--foreground));
  font: inherit;
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.plinth-pagination__page[aria-current="page"] {
  background: rgb(var(--primary));
  color: rgb(var(--primary-foreground));
  font-weight: 600;
}

.plinth-pagination__page:disabled { opacity: 0.4; cursor: not-allowed; }

@media (hover: hover) {
  .plinth-pagination__page:not(:disabled):not([aria-current="page"]):hover {
    background: rgb(var(--accent));
  }
}

.plinth-pagination__ellipsis { padding: 0 0.25rem; color: rgb(var(--muted-foreground)); }
.plinth-pagination__summary { font-size: 0.875rem; color: rgb(var(--muted-foreground)); }

/* ==========================================================================
   DIALOG

   Built on <dialog>, using showModal(). The browser then owns the focus trap,
   the inert backdrop, Escape-to-close and the top-layer stacking — four things
   every hand-rolled modal gets subtly wrong and which @rn-primitives/dialog
   reimplements in JS on top of a portal. The only piece left to write is
   returning focus to the opener, which showModal does not do reliably across
   engines.
   ========================================================================== */

.plinth-dialog {
  width: min(32rem, calc(100vw - 2rem));
  max-height: min(85dvh, 48rem);
  padding: 0;
  border: 1px solid rgb(var(--border));
  border-radius: var(--radius);
  background: rgb(var(--popover));
  color: rgb(var(--popover-foreground));
  box-shadow: 0 16px 48px rgb(0 0 0 / 0.25);
  overflow: hidden;
}

:root[data-theme="dark"] .plinth-dialog,
.dark .plinth-dialog { background: rgb(var(--surface-raised)); }

.plinth-dialog::backdrop {
  background: rgb(0 0 0 / 0.55);
  backdrop-filter: blur(2px);
}

.plinth-dialog[open] { animation: plinth-dialog-in 160ms ease-out; }

@keyframes plinth-dialog-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

.plinth-dialog__inner {
  display: flex;
  flex-direction: column;
  max-height: inherit;
}

.plinth-dialog__header {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1.25rem 1.25rem 0.75rem;
}

.plinth-dialog__title { font-size: 1.125rem; font-weight: 600; margin: 0; flex: 1 1 auto; }
.plinth-dialog__description {
  font-size: 0.875rem;
  color: rgb(var(--muted-foreground));
  margin: 0.375rem 0 0;
}

.plinth-dialog__body { padding: 0 1.25rem 1rem; overflow-y: auto; flex: 1 1 auto; }

.plinth-dialog__footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.75rem;
  padding: 1rem 1.25rem 1.25rem;
  border-top: 1px solid rgb(var(--border));
}

/* On a phone the footer's buttons go full width and stack, so the primary
   action is a thumb-sized target rather than a 90px pill in the corner. */
@media (max-width: 26rem) {
  .plinth-dialog__footer { flex-direction: column-reverse; }
  .plinth-dialog__footer > .plinth-btn { width: 100%; }
}

/* ==========================================================================
   MENU / POPOVER
   ========================================================================== */

.plinth-menu-anchor { position: relative; display: inline-flex; }

.plinth-menu {
  position: absolute;
  z-index: 50;
  min-width: 11rem;
  max-width: min(20rem, calc(100vw - 2rem));
  padding: 0.25rem;
  margin: 0;
  list-style: none;
  border: 1px solid rgb(var(--border));
  border-radius: var(--radius);
  background: rgb(var(--popover));
  color: rgb(var(--popover-foreground));
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.18);
}

:root[data-theme="dark"] .plinth-menu,
.dark .plinth-menu { background: rgb(var(--surface-raised)); }

.plinth-menu--below { top: calc(100% + 0.375rem); }
.plinth-menu--above { bottom: calc(100% + 0.375rem); }
.plinth-menu--start { inset-inline-start: 0; }
.plinth-menu--end { inset-inline-end: 0; }

.plinth-menu__item {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  min-height: 2.5rem;
  padding: 0.375rem 0.625rem;
  border: 0;
  border-radius: calc(var(--radius) - 2px);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.875rem;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}

.plinth-menu__item:disabled,
.plinth-menu__item[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

.plinth-menu__item:hover:not(:disabled),
.plinth-menu__item[data-active="true"] { background: rgb(var(--accent)); }

.plinth-menu__item--danger { color: rgb(var(--destructive)); }
.plinth-menu__label {
  padding: 0.375rem 0.625rem 0.25rem;
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgb(var(--muted-foreground));
}
.plinth-menu__separator { height: 1px; margin: 0.25rem 0.375rem; background: rgb(var(--border)); }

/* ==========================================================================
   TOOLTIP

   CSS-driven, opened by :hover and :focus-within on the wrapper. No JS, no
   portal, no positioning library. The tradeoff is that it cannot escape an
   ancestor with `overflow: hidden` — accepted, because a tooltip is by
   definition supplementary and the accessible name is always on the trigger
   itself via aria-describedby, so nothing is lost when the bubble is clipped.
   ========================================================================== */

.plinth-tooltip { position: relative; display: inline-flex; }

.plinth-tooltip__bubble {
  position: absolute;
  bottom: calc(100% + 0.375rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  padding: 0.375rem 0.5rem;
  border-radius: calc(var(--radius) - 2px);
  background: rgb(var(--foreground));
  color: rgb(var(--background));
  font-size: 0.75rem;
  line-height: 1.3;
  white-space: nowrap;
  max-width: min(18rem, 80vw);
  opacity: 0;
  visibility: hidden;
  transition: opacity 120ms ease-out, visibility 120ms;
  pointer-events: none;
}

.plinth-tooltip:hover .plinth-tooltip__bubble,
.plinth-tooltip:focus-within .plinth-tooltip__bubble {
  opacity: 1;
  visibility: visible;
}

/*
 * A tooltip on a touch device has no hover to open it and no pointer to keep
 * it open, so it flashes on tap and reads as a glitch. Hidden entirely there;
 * the trigger's aria-label carries the same text for a screen reader.
 */
@media (hover: none) {
  .plinth-tooltip__bubble { display: none; }
}

/* ==========================================================================
   TOAST
   ========================================================================== */

.plinth-toasts {
  position: fixed;
  z-index: 100;
  bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  inset-inline: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: center;
  pointer-events: none;
}

@media (min-width: 40rem) {
  .plinth-toasts { inset-inline-start: auto; align-items: flex-end; max-width: 26rem; }
}

.plinth-toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  width: 100%;
  max-width: 26rem;
  padding: 0.75rem 1rem;
  border: 1px solid rgb(var(--border));
  border-inline-start-width: 3px;
  border-radius: var(--radius);
  background: rgb(var(--popover));
  color: rgb(var(--popover-foreground));
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.2);
  animation: plinth-toast-in 180ms ease-out;
}

:root[data-theme="dark"] .plinth-toast,
.dark .plinth-toast { background: rgb(var(--surface-raised)); }

.plinth-toast--success { border-inline-start-color: rgb(var(--success)); }
.plinth-toast--error { border-inline-start-color: rgb(var(--destructive)); }
.plinth-toast--info { border-inline-start-color: rgb(var(--focus-ring)); }
.plinth-toast--warning { border-inline-start-color: rgb(var(--warning)); }

.plinth-toast__body { flex: 1 1 auto; min-width: 0; }
.plinth-toast__title { font-weight: 600; font-size: 0.875rem; }
.plinth-toast__message { font-size: 0.875rem; color: rgb(var(--muted-foreground)); margin-top: 0.125rem; }

@keyframes plinth-toast-in {
  from { opacity: 0; transform: translateY(0.5rem); }
  to { opacity: 1; transform: none; }
}

/* ==========================================================================
   AVATAR
   ========================================================================== */

.plinth-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgb(var(--secondary));
  color: rgb(var(--secondary-foreground));
  font-weight: 600;
  overflow: hidden;
  flex: 0 0 auto;
  user-select: none;
}

.plinth-avatar img { width: 100%; height: 100%; object-fit: cover; }

/* ==========================================================================
   NAV CHROME
   ========================================================================== */

.plinth-appbar {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 3.5rem;
  padding: 0 1rem;
  padding-top: env(safe-area-inset-top, 0px);
  border-bottom: 1px solid rgb(var(--border));
  background: rgb(var(--background) / 0.9);
  backdrop-filter: blur(8px);
}

.plinth-appbar__title { font-size: 1rem; font-weight: 600; }

.plinth-appbar__nav { display: none; gap: 0.25rem; align-items: center; }

@media (min-width: 48rem) {
  .plinth-appbar__nav { display: flex; }
}

.plinth-navlink {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 2.5rem;
  padding: 0 0.75rem;
  border-radius: calc(var(--radius) - 2px);
  color: rgb(var(--muted-foreground));
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration: none;
}

.plinth-navlink[aria-current="page"] {
  background: rgb(var(--accent));
  color: rgb(var(--foreground));
}

@media (hover: hover) {
  .plinth-navlink:hover { color: rgb(var(--foreground)); background: rgb(var(--accent)); }
}

.plinth-bottomnav {
  position: sticky;
  bottom: 0;
  z-index: 40;
  display: flex;
  border-top: 1px solid rgb(var(--border));
  background: rgb(var(--background));
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

@media (min-width: 48rem) {
  .plinth-bottomnav { display: none; }
}

.plinth-bottomnav__item {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.125rem;
  min-height: 3.25rem;
  padding: 0.375rem 0.25rem;
  border: 0;
  background: transparent;
  color: rgb(var(--muted-foreground));
  font: inherit;
  font-size: 0.6875rem;
  text-decoration: none;
  cursor: pointer;
}

.plinth-bottomnav__item[aria-current="page"] { color: rgb(var(--foreground)); }

/* ==========================================================================
   PIN PAD
   ========================================================================== */

.plinth-pinpad { display: flex; flex-direction: column; align-items: center; gap: 1.5rem; }

.plinth-pinpad__dots { display: flex; gap: 0.75rem; }

.plinth-pinpad__dot {
  width: 0.875rem;
  height: 0.875rem;
  border-radius: 50%;
  border: 1px solid rgb(var(--muted-foreground));
  transition: background-color 90ms ease-out, transform 90ms ease-out;
}

.plinth-pinpad__dot--filled {
  background: rgb(var(--foreground));
  border-color: rgb(var(--foreground));
  transform: scale(1.1);
}

.plinth-pinpad__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.75rem;
  width: min(17rem, 100%);
}

.plinth-pinpad__key {
  aspect-ratio: 1;
  min-height: 3.5rem;
  border: 1px solid rgb(var(--border));
  border-radius: 50%;
  background: rgb(var(--card));
  color: rgb(var(--foreground));
  font-family: var(--font-mono);
  font-size: 1.5rem;
  cursor: pointer;
  transition: transform 90ms ease-out, background-color 90ms ease-out;
}

.plinth-pinpad__key:active { transform: scale(0.92); background: rgb(var(--accent)); }
.plinth-pinpad__key--blank { visibility: hidden; }

.plinth-pinpad--shake { animation: plinth-shake 400ms ease-in-out; }

@keyframes plinth-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-8px); }
  40% { transform: translateX(8px); }
  60% { transform: translateX(-5px); }
  80% { transform: translateX(3px); }
}

/* ==========================================================================
   TOTP
   ========================================================================== */

.plinth-totp { display: inline-flex; align-items: center; gap: 0.75rem; }

.plinth-totp__code {
  font-family: var(--font-mono);
  font-size: 1.5rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.12em;
}

.plinth-totp__ring { transform: rotate(-90deg); flex: 0 0 auto; }
.plinth-totp__ring circle { transition: stroke-dashoffset 1s linear; }
.plinth-totp--expiring .plinth-totp__code { color: rgb(var(--destructive)); }

/* ==========================================================================
   BRAND
   ========================================================================== */

.plinth-brand { display: inline-flex; align-items: center; gap: 0.625rem; }
.plinth-brand__word { font-weight: 700; letter-spacing: -0.01em; }
.plinth-brand__byline { font-size: 0.75rem; color: rgb(var(--muted-foreground)); }

/* ==========================================================================
   AUTH SHELL
   ========================================================================== */

.plinth-auth {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 1.5rem 1rem;
  background: rgb(var(--background));
}

.plinth-auth__card { width: 100%; max-width: 24rem; }
.plinth-auth__brand { display: flex; justify-content: center; margin-bottom: 1.5rem; }
.plinth-auth__footer {
  margin-top: 1.5rem;
  text-align: center;
  font-size: 0.875rem;
  color: rgb(var(--muted-foreground));
}

/* ==========================================================================
   ALERT / FORM ERROR
   ========================================================================== */

.plinth-alert {
  display: flex;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border: 1px solid;
  border-radius: var(--radius);
  font-size: 0.875rem;
}

.plinth-alert--error {
  border-color: rgb(var(--destructive) / 0.4);
  background: rgb(var(--destructive) / 0.1);
  color: rgb(var(--destructive));
}

:root[data-theme="dark"] .plinth-alert--error,
.dark .plinth-alert--error { color: rgb(248 113 113); }

.plinth-alert--success {
  border-color: rgb(var(--success) / 0.4);
  background: rgb(var(--success) / 0.1);
  color: rgb(var(--success));
}

.plinth-alert--info {
  border-color: rgb(var(--border));
  background: rgb(var(--secondary));
  color: rgb(var(--foreground));
}

/* ==========================================================================
   SCROLLBARS
   Only where a pointer exists: a touch device has an overlay scrollbar the OS
   draws and styling it does nothing but cost bytes.
   ========================================================================== */

@media (hover: hover) {
  .plinth-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
  .plinth-scroll::-webkit-scrollbar-track { background: transparent; }
  .plinth-scroll::-webkit-scrollbar-thumb {
    background: rgb(var(--muted-foreground) / 0.3);
    border-radius: 4px;
  }
  .plinth-scroll::-webkit-scrollbar-thumb:hover {
    background: rgb(var(--muted-foreground) / 0.5);
  }
}

/* ==========================================================================
   FONTS — Geist and Geist Mono, self-hosted (style/fonts/, OFL 1.1).
   Apps ship the directory beside their CSS (see style/fonts/README.md).
   ========================================================================== */
@font-face { font-family: "Geist"; src: url("fonts/Geist-Regular.ttf") format("truetype"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Geist"; src: url("fonts/Geist-Medium.ttf") format("truetype"); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Geist"; src: url("fonts/Geist-Bold.ttf") format("truetype"); font-weight: 600 900; font-style: normal; font-display: swap; }
@font-face { font-family: "Geist Mono"; src: url("fonts/GeistMono-Regular.ttf") format("truetype"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Geist Mono"; src: url("fonts/GeistMono-Medium.ttf") format("truetype"); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Geist Mono"; src: url("fonts/GeistMono-Bold.ttf") format("truetype"); font-weight: 600 900; font-style: normal; font-display: swap; }

/* ==========================================================================
   RAISED SURFACES — one token for every theme. The dark lift used to be a
   literal under [data-theme=dark] only, so system-dark users got flat black
   dialogs, menus and toasts.
   ========================================================================== */
.plinth-card,
.plinth-dialog,
.plinth-menu,
.plinth-toast { background: rgb(var(--surface-raised)); }

/* ==========================================================================
   ALERT — warning tone (alongside info, error, success).
   ========================================================================== */
.plinth-alert--warning {
  border-color: rgb(var(--warning) / 0.4);
  background: rgb(var(--warning) / 0.1);
  color: rgb(var(--foreground));
}

/* ==========================================================================
   LOG VIEW — machine output, one line per row: time, source, text. Mono,
   newest last, follows the tail while the reader is at the bottom. Tone is
   carried by a text marker as well as colour (errors are prefixed), never
   by colour alone.
   ========================================================================== */
.plinth-log {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  line-height: 1.25rem;
  background: rgb(var(--secondary));
  border: 1px solid rgb(var(--border));
  border-radius: var(--radius);
  max-height: var(--plinth-log-height, 28rem);
  overflow: auto;
  padding: 0.5rem 0;
  overscroll-behavior: contain;
}
.plinth-log__line {
  display: grid;
  grid-template-columns: 4.75rem 6.5rem 1fr;
  gap: 0.75rem;
  padding: 0 0.75rem;
}
.plinth-log--nowrap .plinth-log__text { white-space: pre; }
.plinth-log__text { white-space: pre-wrap; overflow-wrap: anywhere; min-width: 0; }
.plinth-log__time, .plinth-log__source { color: rgb(var(--muted-foreground)); white-space: nowrap; }
.plinth-log__line--error { color: rgb(var(--destructive)); background: rgb(var(--destructive) / 0.08); }
:root[data-theme="dark"] .plinth-log__line--error,
.dark .plinth-log__line--error { color: rgb(248 113 113); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .plinth-log__line--error { color: rgb(248 113 113); }
}
.plinth-log__line--warning { color: rgb(var(--warning)); }
.plinth-log__line--success { color: rgb(var(--success)); }
.plinth-log__empty { padding: 1rem 0.75rem; color: rgb(var(--muted-foreground)); }
@media (max-width: 40rem) {
  .plinth-log__line { grid-template-columns: 4.75rem 1fr; }
  .plinth-log__source { display: none; }
}

/* ==========================================================================
   TABS — an in-page tablist (WAI-ARIA tabs: roving tabindex, arrow keys,
   Home/End). Not navigation: routes stay links.
   ========================================================================== */
.plinth-tabs__list {
  display: flex;
  gap: 0.25rem;
  border-bottom: 1px solid rgb(var(--border));
  overflow-x: auto;
  scrollbar-width: none;
}
.plinth-tabs__tab {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 0.625rem 0.875rem;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: rgb(var(--muted-foreground));
  cursor: pointer;
  white-space: nowrap;
  transition: color 120ms ease-out, border-color 120ms ease-out;
}
.plinth-tabs__tab[aria-selected="true"] { color: rgb(var(--foreground)); border-bottom-color: rgb(var(--foreground)); }
@media (hover: hover) { .plinth-tabs__tab:hover { color: rgb(var(--foreground)); } }
.plinth-tabs__tab:focus-visible { outline-offset: -2px; }
.plinth-tabs__panel { padding-top: 1.25rem; }

/* ==========================================================================
   KEY-VALUE — a real <dl>: label column, value column; stacks on narrow
   screens. Labels are the estate's mono micro-caps.
   ========================================================================== */
.plinth-kv {
  display: grid;
  grid-template-columns: minmax(8rem, max-content) 1fr;
  gap: 0.5rem 1.5rem;
  margin: 0;
  font-size: 0.875rem;
}
.plinth-kv__key {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  line-height: 1.25rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgb(var(--muted-foreground));
}
.plinth-kv__value { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.plinth-kv__value--mono { font-family: var(--font-mono); font-size: 0.8125rem; }
@media (max-width: 40rem) {
  .plinth-kv { grid-template-columns: 1fr; gap: 0.125rem; }
  .plinth-kv__value { margin-bottom: 0.625rem; }
}

/* ==========================================================================
   SELECT — the native <select>, styled to sit with Input (same height,
   border, radius and focus), so keyboard, screen-reader and mobile pickers
   all behave natively.
   ========================================================================== */
.plinth-select {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 3rem;
  padding: 0 2.5rem 0 0.75rem;
  border: 1px solid rgb(var(--input));
  border-radius: var(--radius);
  background-color: rgb(var(--background));
  background-image: linear-gradient(45deg, transparent 50%, rgb(var(--muted-foreground)) 50%),
    linear-gradient(135deg, rgb(var(--muted-foreground)) 50%, transparent 50%);
  background-position: calc(100% - 1.125rem) 50%, calc(100% - 0.8125rem) 50%;
  background-size: 0.3125rem 0.3125rem;
  background-repeat: no-repeat;
  color: rgb(var(--foreground));
  font: inherit;
  font-size: max(1rem, 16px);
  transition: border-color 120ms ease-out;
}
.plinth-select:disabled { opacity: 0.5; cursor: not-allowed; }
.plinth-select--sm { height: 2.5rem; font-size: 0.875rem; }

/* ==========================================================================
   STATUS — a dot and a word, the estate's status idiom (colour is never the
   only channel). `--live` pulses for work in flight; reduced motion stops it.
   ========================================================================== */
.plinth-status {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
}
.plinth-status__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: rgb(var(--muted-foreground));
  flex: none;
}
.plinth-status--success .plinth-status__dot { background: rgb(var(--success)); }
.plinth-status--warning .plinth-status__dot { background: rgb(var(--warning)); }
.plinth-status--danger .plinth-status__dot { background: rgb(var(--destructive)); }
.plinth-status--info .plinth-status__dot { background: rgb(var(--info)); }
.plinth-status--live .plinth-status__dot { animation: plinth-pulse 1.4s ease-in-out infinite; }

/* ==========================================================================
   WIDGET BOARD — components/board: a 12-column grid of fixed-height rows
   (--row, --gap from the board's style), widgets placed by --x/--y/--w/--h.
   Lifted from Bootstrap's dashboard; `has-handles` adds the grip, menu and
   resize corner, `is-editing` is Bootstrap's edit mode, `is-narrow` stacks
   the widgets in reading order (`order`), one column.
   ========================================================================== */
.plinth-board-wrap { position: relative; }
.plinth-board {
  --col: calc((100% - 11 * var(--gap)) / 12);
  position: relative;
  height: calc(var(--rows) * (var(--row) + var(--gap)) - var(--gap));
  min-height: var(--row);
}
.plinth-board.is-editing,
.plinth-board.is-dragging {
  user-select: none;
  background-image:
    repeating-linear-gradient(to right, rgb(var(--muted) / 0.45) 0 var(--col), transparent var(--col) calc(var(--col) + var(--gap))),
    repeating-linear-gradient(to bottom, transparent 0 var(--row), rgb(var(--background)) var(--row) calc(var(--row) + var(--gap)));
  border-radius: var(--radius);
}
.plinth-board__empty { color: rgb(var(--muted-foreground)); font-size: 0.875rem; margin: 0; }

.plinth-widget {
  position: absolute; display: flex; flex-direction: column; min-width: 0; overflow: hidden;
  left: calc(var(--x) * (var(--col) + var(--gap)));
  top: calc(var(--y) * (var(--row) + var(--gap)));
  width: calc(var(--w) * var(--col) + (var(--w) - 1) * var(--gap));
  height: calc(var(--h) * var(--row) + (var(--h) - 1) * var(--gap));
  border: 1px solid rgb(var(--border)); border-radius: var(--radius);
  background: rgb(var(--surface-raised));
  transition: left 160ms ease-out, top 160ms ease-out, width 160ms ease-out, height 160ms ease-out, box-shadow 120ms;
  /* Scrolling or updating one widget repaints that widget only. */
  contain: layout paint style;
}
.plinth-widget:focus-visible { outline: 2px solid rgb(var(--focus-ring)); outline-offset: 2px; }
.plinth-widget__head {
  flex: none; display: flex; align-items: center; gap: 0.25rem; height: 2.25rem; padding: 0 0.375rem 0 0.875rem;
}
.has-handles .plinth-widget__head { padding-left: 0.25rem; }
.plinth-widget__title {
  margin: 0; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.12em; color: rgb(var(--muted-foreground));
}
.plinth-widget__body { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 0 0.875rem 0.75rem; }
.plinth-lazy { height: 100%; }
.plinth-widget__grip,
.plinth-widget__remove,
.plinth-widget__menu .plinth-icon-btn {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 1.75rem; height: 1.75rem; min-height: 0; padding: 0;
  border: 0; border-radius: calc(var(--radius) - 2px); background: none;
  color: rgb(var(--muted-foreground)); cursor: pointer;
}
.plinth-widget__grip { cursor: grab; touch-action: none; opacity: 0.55; }
.plinth-widget:hover .plinth-widget__grip, .plinth-widget__grip:focus-visible { opacity: 1; }
.plinth-widget__grip:focus-visible,
.plinth-widget__remove:focus-visible { outline: 2px solid rgb(var(--focus-ring)); outline-offset: 0; }
@media (hover: hover) {
  .plinth-widget__grip:hover, .plinth-widget__remove:hover, .plinth-widget__menu .plinth-icon-btn:hover { background: rgb(var(--accent)); color: rgb(var(--foreground)); }
}
.plinth-widget__menu { flex: none; display: inline-flex; }
.plinth-widget__resize {
  position: absolute; right: 0; bottom: 0; width: 1rem; height: 1rem; cursor: nwse-resize; touch-action: none;
  background: linear-gradient(135deg, transparent 50%, rgb(var(--muted-foreground) / 0.6) 50% 60%, transparent 60% 70%, rgb(var(--muted-foreground) / 0.6) 70% 80%, transparent 80%);
}
.has-handles .plinth-widget__resize { opacity: 0; transition: opacity 120ms; }
.has-handles .plinth-widget:hover .plinth-widget__resize,
.has-handles.is-dragging .plinth-widget__resize { opacity: 1; }
@media (hover: none) { .has-handles .plinth-widget__resize { opacity: 1; } }

/* Edit mode: grab anywhere (the content goes inert, so a grab cannot also
   click a table row), resize from the corner, remove with ×. */
.is-editing .plinth-widget { cursor: grab; touch-action: none; box-shadow: 0 1px 0 rgb(0 0 0 / 0.04); }
.is-editing .plinth-widget__body { pointer-events: none; }
.is-dragging .plinth-widget, .is-dragging .plinth-widget__grip { cursor: grabbing; }
.is-dragging .plinth-widget__body { pointer-events: none; }

/* While dragging: the dragged widget lifts and follows the pointer, the
   landing spot is outlined, and widgets the drop would move are marked. */
.plinth-widget.is-dragged { z-index: 20; transition: none; opacity: 0.7; box-shadow: 0 12px 32px rgb(0 0 0 / 0.25); }
.plinth-widget.is-displaced::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: rgb(var(--warning) / 0.18); box-shadow: inset 0 0 0 2px rgb(var(--warning) / 0.7);
}
.plinth-board__landing {
  position: absolute; z-index: 25; pointer-events: none;
  left: calc(var(--x) * (var(--col) + var(--gap)));
  top: calc(var(--y) * (var(--row) + var(--gap)));
  width: calc(var(--w) * var(--col) + (var(--w) - 1) * var(--gap));
  height: calc(var(--h) * var(--row) + (var(--h) - 1) * var(--gap));
  border-radius: var(--radius);
  background: rgb(var(--success) / 0.2); box-shadow: inset 0 0 0 2px rgb(var(--success) / 0.75);
  transition: left 90ms, top 90ms, width 90ms, height 90ms;
}
.plinth-widget--ghost {
  position: fixed; z-index: 60; width: 14rem; height: auto; left: 0; top: 0; pointer-events: none;
  opacity: 0.92; box-shadow: 0 12px 32px rgb(0 0 0 / 0.25); transition: none;
}

/* One column: stacked in reading order, same heights; reorder by menu or
   the grip's up and down arrows. */
.plinth-board.is-narrow { height: auto !important; display: flex; flex-direction: column; gap: var(--gap); }
.plinth-board.is-narrow .plinth-widget {
  position: relative; left: auto; top: auto; width: auto;
  height: auto; min-height: calc(2 * var(--row)); max-height: calc(var(--h) * var(--row) + (var(--h) - 1) * var(--gap));
}
.plinth-board.is-narrow .plinth-board__landing,
.plinth-board.is-narrow .plinth-widget__resize { display: none; }
.plinth-board.is-narrow .plinth-widget__grip { touch-action: auto; cursor: default; }

/* The drawer: a panel at the right edge. `.has-tab` is Bootstrap's
   auto-hiding taskbar (hover or focus the tab to slide it out); `.is-tucked`
   hides it while a widget is dragged out of it. */
.plinth-wdrawer {
  --tab: 0rem;
  position: fixed; z-index: 40; right: 0; top: 4rem; bottom: 1rem; width: min(22rem, 100vw);
  display: flex; transform: translateX(100%); visibility: hidden;
  transition: transform 200ms ease-out, visibility 0s linear 200ms;
}
.plinth-wdrawer.has-tab { --tab: 2rem; transform: translateX(calc(100% - var(--tab))); visibility: visible; }
.plinth-wdrawer.is-open,
.plinth-wdrawer.has-tab:hover,
.plinth-wdrawer.has-tab:focus-within { transform: none; visibility: visible; transition: transform 200ms ease-out; }
.plinth-wdrawer.is-tucked { transform: translateX(calc(100% - var(--tab))); }
.plinth-wdrawer__tab {
  flex: none; width: var(--tab); align-self: center; height: 7.5rem; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgb(var(--border)); border-right: 0; border-radius: var(--radius) 0 0 var(--radius);
  background: rgb(var(--background)); color: rgb(var(--muted-foreground)); cursor: pointer;
  font-family: var(--font-mono); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.12em;
}
.plinth-wdrawer__tab span { writing-mode: vertical-rl; transform: rotate(180deg); }
.plinth-wdrawer__panel {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.625rem; padding: 0.875rem;
  border: 1px solid rgb(var(--border)); border-right: 0; border-radius: var(--radius) 0 0 var(--radius);
  background: rgb(var(--background)); box-shadow: -6px 0 24px rgb(0 0 0 / 0.14);
}
.plinth-wdrawer__head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.plinth-wdrawer__title,
.plinth-wdrawer__grouptitle {
  margin: 0; font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.12em; color: rgb(var(--muted-foreground));
}
.plinth-wdrawer__title { color: rgb(var(--foreground)); }
.plinth-wdrawer__close {
  display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; padding: 0;
  border: 0; border-radius: calc(var(--radius) - 2px); background: none; color: rgb(var(--muted-foreground)); cursor: pointer;
}
.plinth-wdrawer__close:hover { background: rgb(var(--accent)); color: rgb(var(--foreground)); }
.plinth-wdrawer__search {
  flex: none; box-sizing: border-box; width: 100%; height: 2.25rem; padding: 0 0.75rem; font: inherit; font-size: 0.875rem;
  border: 1px solid rgb(var(--input, var(--border))); border-radius: var(--radius);
  background: rgb(var(--background)); color: rgb(var(--foreground));
}
.plinth-wdrawer__search:focus-visible { outline: 2px solid rgb(var(--focus-ring)); outline-offset: 1px; }
.plinth-wdrawer__list { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 0.875rem; }
.plinth-wdrawer__group { display: flex; flex-direction: column; gap: 0.375rem; }
.plinth-wdrawer__empty { margin: 0; font-size: 0.8125rem; color: rgb(var(--muted-foreground)); }
.plinth-wdrawer__item {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.125rem 0.5rem; width: 100%; padding: 0.5rem 0.75rem; text-align: left;
  border: 1px dashed rgb(var(--border)); border-radius: var(--radius); background: rgb(var(--surface-raised));
  color: inherit; font: inherit; cursor: grab; user-select: none;
}
.plinth-wdrawer__item:disabled { cursor: default; opacity: 0.6; border-style: solid; }
@media (hover: hover) { .plinth-wdrawer__item:not(:disabled):hover { border-style: solid; border-color: rgb(var(--foreground) / 0.4); } }
.plinth-wdrawer__item:focus-visible { outline: 2px solid rgb(var(--focus-ring)); outline-offset: 2px; }
.plinth-wdrawer__name { flex: 1; font-size: 0.875rem; font-weight: 500; }
.plinth-wdrawer__chip {
  font-family: var(--font-mono); font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: rgb(var(--muted-foreground)); border: 1px solid rgb(var(--border)); border-radius: 999px; padding: 0.0625rem 0.4375rem;
}
.plinth-wdrawer__desc { flex-basis: 100%; font-size: 0.75rem; color: rgb(var(--muted-foreground)); }
.plinth-wdrawer__foot { display: flex; justify-content: flex-end; border-top: 1px solid rgb(var(--border)); padding-top: 0.5rem; }
.plinth-wsettings__for { margin: 0 0 0.75rem; font-weight: 500; }
@media (max-width: 40rem) {
  .plinth-wdrawer { top: auto; bottom: 0; height: 75vh; width: 100vw; transform: translateY(100%); }
  .plinth-wdrawer.is-open { transform: none; }
  .plinth-wdrawer__panel { border-right: 1px solid rgb(var(--border)); border-bottom: 0; border-radius: var(--radius) var(--radius) 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  .plinth-widget, .plinth-board__landing, .plinth-wdrawer { transition: none; }
}
:root[data-motion="reduced"] .plinth-widget,
:root[data-motion="reduced"] .plinth-board__landing,
:root[data-motion="reduced"] .plinth-wdrawer { transition: none; }
