/* No @import here: an @import at the top of the only stylesheet serialises
 * HTML -> CSS -> CSS -> font. The three faces below load from page() in
 * src/html.ts instead, as these exact tags, in this order, before the
 * app.css <link> (all three verified reachable from this box):
 *
 *   <link rel="preconnect" href="https://fonts.googleapis.com">
 *   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 *   <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter+Tight:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap">
 *
 * Until those tags exist the stacks below fall back to the system faces. */

/* Hallmark - macrostructure: Workbench - theme: Cobalt - genre: modern-minimal
 * tone: technical, calm-confident - anchor hue: electric cobalt (256)
 * nav: N1b bordered bar with wordmark + rail - footer: none (app chrome, not a page)
 * states shipped: default - hover - focus-visible - active - disabled - error
 * (empty and success are page-level; the .btn[data-state] rules below are the
 * contract for the async buttons Phase 1 adds - nothing emits them yet)
 * contrast: pass (40-41) - tokens: pass (48) - responsive: pass (49) - mobile: pass (34, 50-56)
 * pre-emit critique: P4 H5 E5 S5 R5 V4
 *
 * Kopet's only stylesheet (PLAN section 2, D11). One hand-written file, no
 * framework, no build step, no CDN except the font link above.
 *
 * Why Cobalt: Kopet is an instrument panel for one developer at a time - a cool
 * engineered sheet, hairlines instead of boxes, and exactly one signal colour
 * spent on the thing that is live right now. The theme is a light theme with a
 * graphite band; here the graphite band is the whole surface in dark mode,
 * because an app the user stares at all evening has to answer
 * prefers-color-scheme and Cobalt's graphite is already the right dark.
 *
 * Reading order: tokens, dark tokens, reset, primitives, components.
 * Every colour and every font in this file comes through a token. If a value is
 * needed that is not here, add the token first. */

/* ------------------------------------------------------------------ tokens */

:root {
  color-scheme: light dark;

  /* Paper is cool near-white, never #fff; ink is cool charcoal, never #000.
     Three paper steps: the page, a raised surface, the work surface behind
     the app frame. */
  --color-paper: oklch(98.5% 0.004 250);
  --color-paper-2: oklch(96.4% 0.006 252);
  --color-paper-3: oklch(93.8% 0.008 254);

  --color-ink: oklch(24% 0.02 258);
  --color-ink-2: oklch(34% 0.018 257);
  --color-muted: oklch(50% 0.016 256);

  /* Hairlines do the structure work. --color-rule divides, --color-rule-2
     encloses a control the user can touch - so it is the only boundary that
     control has, and it clears 3:1 on paper and paper-2 in both schemes. */
  --color-rule: oklch(89% 0.008 254);
  --color-rule-2: oklch(64% 0.014 255);

  /* The one signal. Under 5% of any viewport: the primary button, the focus
     ring, the running dot, the current nav item, the selection. Its lightness
     is set by two contrast floors at once - near-white ink on it (5.0:1) and
     it as a link colour on paper (4.9:1) - not by taste. */
  --color-accent: oklch(54% 0.19 256);
  --color-accent-ink: oklch(99% 0.004 250);
  --color-accent-soft: oklch(95.5% 0.032 256);
  --color-focus: oklch(52% 0.20 256);

  /* Graphite is the machine's own surface: log panes, code, the terminal. It
     stays dark in both schemes so a log always reads as a log. */
  --color-graphite: oklch(22% 0.016 260);
  --color-graphite-ink: oklch(92% 0.01 258);

  /* Status is semantic, not brand. A crashed app is red because it is broken,
     not because red is in the palette. */
  --color-ok: oklch(52% 0.12 155);
  --color-ok-soft: oklch(96% 0.032 155);
  --color-ok-ink: oklch(99% 0.004 155);
  --color-danger: oklch(52% 0.19 27);
  --color-danger-soft: oklch(96.5% 0.028 27);
  --color-danger-ink: oklch(99% 0.004 27);
  --color-warn: oklch(64% 0.14 75);

  /* Type. Space Grotesk sets the wordmark and the headings; Inter Tight is the
     body workhorse and carries the whole label register too. JetBrains Mono is
     the outlier and spends its one slot on machine output - code, logs, ports,
     paths, usernames, ids. It was doing label work as well (field labels,
     column heads, badges); those are words, not output, so they went back to
     the body face in uppercase, and the outlier stays rare enough to mean
     something. Three families is the ceiling. */
  --font-display: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --font-body: "Inter Tight", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Major third (1.25) off a 16px body. Five sizes on any one page. */
  --text-sm: 0.8rem;
  --text-base: 1rem;
  --text-md: 1.25rem;
  --text-lg: 1.5625rem;
  --text-xl: 1.9531rem;

  --lh-tight: 1.12;
  --lh-body: 1.55;
  --tracking-display: -0.025em;
  --tracking-label: 0.06em;

  /* 4pt scale. Nothing in this file pads with a number that is not here. */
  --space-3xs: 2px;
  --space-2xs: 4px;
  --space-xs: 8px;
  --space-sm: 12px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-2xl: 48px;
  --space-3xl: 64px;

  /* Drawn with a ruler: 6px on controls, 10px on surfaces, 4px on a tag. */
  --radius-xs: 4px;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-pill: 999px;

  /* Depth is borders. These two are the only shadows: a barely-there lift on a
     raised surface, and a real one under the drawer because it floats over the
     app frame and has to read as over. */
  --shadow-1: 0 1px 2px oklch(24% 0.02 258 / 0.06);
  --shadow-2: 0 12px 40px oklch(24% 0.02 258 / 0.16);

  --dur-1: 120ms;
  --dur-2: 190ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* Layout constants. --nav-h is a token because the toolbar and the split
     both dock against it; two sticky bars at top: 0 overlap. */
  --nav-h: 56px;
  /* Only the phone layout reads this, to size the preview to the rest of the
     first screen. It is the height of the two-row toolbar below 60rem - the
     project's name on one line, the controls on the next: one control height
     plus the name's line and the padding around both, so it follows the touch
     step in --control-h instead of being re-measured. It is a floor rather
     than a lock - if the toolbar ever wraps taller, the preview runs a little
     past the fold instead of the layout breaking. Above 60rem the toolbar's
     real height is subtracted by the flex column in .workbench, and nothing
     reads this. */
  --toolbar-h: calc(var(--control-h) + var(--space-2xl));
  --measure: 1120px;
  --prose: 65ch;
  --chat-w: 380px;
  /* One height for every control. A finger needs 44px; a mouse does not, and
     the denser control is better on a toolbar full of them, so the token moves
     and the inputs and buttons move with it, together. */
  --control-h: 40px;

  --z-sticky: 200;
  --z-nav: 300;
  --z-drawer: 400;
  --z-top: 500;
}

@media (pointer: coarse) {
  :root {
    --control-h: 44px;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    /* Cobalt's graphite band, taken as the ground. Same hue family as light,
       so the app does not change temperature when the sun goes down. */
    --color-paper: oklch(19.5% 0.015 260);
    --color-paper-2: oklch(23% 0.016 260);
    --color-paper-3: oklch(15.5% 0.014 260);

    --color-ink: oklch(95% 0.008 258);
    --color-ink-2: oklch(87% 0.01 258);
    --color-muted: oklch(69% 0.015 258);

    --color-rule: oklch(31% 0.014 260);
    --color-rule-2: oklch(52% 0.016 260);

    /* The accent lifts and de-saturates a little so it clears 4.5:1 on a dark
       ground; its ink flips to near-black for the same reason. */
    --color-accent: oklch(72% 0.15 256);
    --color-accent-ink: oklch(18% 0.03 258);
    --color-accent-soft: oklch(30% 0.055 256);
    --color-focus: oklch(74% 0.15 256);

    --color-graphite: oklch(14% 0.013 260);
    --color-graphite-ink: oklch(90% 0.01 258);

    --color-ok: oklch(74% 0.13 155);
    --color-ok-soft: oklch(27% 0.05 155);
    --color-ok-ink: oklch(17% 0.03 155);
    --color-danger: oklch(71% 0.15 27);
    --color-danger-soft: oklch(28% 0.055 27);
    --color-danger-ink: oklch(17% 0.03 27);
    --color-warn: oklch(78% 0.13 75);

    --shadow-1: 0 1px 2px oklch(8% 0.01 260 / 0.45);
    --shadow-2: 0 12px 40px oklch(8% 0.01 260 / 0.55);
  }
}

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

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

/* The one !important in this file, and it earns it. A component that sets its
   own display - .error-bar, .row, .btn, the drawer - is a 0-1-0 selector and
   the UA's [hidden] { display: none } is 0-0-1, so any such component shipped
   with the hidden attribute stays on screen as an empty box. The live layer
   hides and shows elements with el.hidden, so this has to hold for every
   component, not for the one where it was noticed. */
[hidden] {
  display: none !important;
}

/* clip, not hidden: hidden on the root kills position: sticky on the nav. */
html,
body {
  overflow-x: clip;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--color-paper);
  color: var(--color-ink-2);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 400;
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
  margin: 0;
}

ul,
ol {
  margin: 0;
  padding: 0;
}

img,
svg,
video,
canvas,
iframe {
  display: block;
  max-width: 100%;
}

svg {
  flex: none;
}

/* Forms inherit nothing by default; this is the one place it is worth saying
   so, because every control in this file assumes it. */
button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
  margin: 0;
}

textarea {
  resize: vertical;
}

a {
  color: var(--color-accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  transition-property: color, text-decoration-color;
  transition-duration: var(--dur-1);
  transition-timing-function: var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  a:hover {
    color: var(--color-ink);
  }
}

/* Every affordance answers a press, not just the buttons. */
a:active {
  color: color-mix(in oklab, var(--color-accent) 68%, var(--color-ink));
}

/* One focus ring for the whole app. It appears instantly - never transition an
   outline, or the first frame of a keyboard focus is invisible. */
:focus {
  outline: none;
}

:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: inherit;
}

code,
kbd,
pre,
samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

pre {
  margin: 0;
  padding: var(--space-md);
  background: var(--color-graphite);
  color: var(--color-graphite-ink);
  border-radius: var(--radius-md);
  overflow-x: auto;
  line-height: 1.5;
  tab-size: 2;
}

code:not(pre code) {
  padding: var(--space-3xs) var(--space-2xs);
  background: var(--color-paper-2);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-xs);
}

hr {
  height: 0;
  margin: var(--space-lg) 0;
  border: 0;
  border-block-start: 1px solid var(--color-rule);
}

::selection {
  background: var(--color-accent);
  color: var(--color-accent-ink);
}

/* ---------------------------------------------------------------- headings */

.h1,
.h2 {
  font-family: var(--font-display);
  font-style: normal;
  color: var(--color-ink);
  letter-spacing: var(--tracking-display);
  line-height: var(--lh-tight);
  /* A project name is user input and can be one long unbroken word. */
  overflow-wrap: anywhere;
  min-width: 0;
}

.h1 {
  font-size: var(--text-xl);
  font-weight: 600;
}

.h2 {
  font-size: var(--text-lg);
  font-weight: 600;
}

.muted {
  color: var(--color-muted);
}

/* Identifiers - a username, a port, a uuid - read as data, not as prose. */
.mono {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

/* --------------------------------------------------------------- skip link */

.skip-link {
  position: fixed;
  inset-block-start: var(--space-xs);
  inset-inline-start: var(--space-xs);
  z-index: var(--z-top);
  padding: var(--space-xs) var(--space-md);
  background: var(--color-accent);
  color: var(--color-accent-ink);
  border-radius: var(--radius-sm);
  text-decoration: none;
  font-weight: 500;
  white-space: nowrap;
  /* Off-screen until focused, and not animated on the way in. */
  transform: translateY(calc(-100% - var(--space-md)));
}

.skip-link:focus-visible {
  transform: none;
}

/* --------------------------------------------------------------------- nav */

.nav {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-nav);
  height: var(--nav-h);
  background: color-mix(in oklab, var(--color-paper) 86%, transparent);
  backdrop-filter: blur(8px);
  border-block-end: 1px solid var(--color-rule);
}

/* Mobile first, and it collapses for real: the bar is a fixed-height rail with
   exactly three things in it - wordmark, destinations, log out. The signed-in
   name is the one item that is neither a destination nor a control, so it is
   what leaves the rail below 30rem; the rail itself never wraps, never
   shrinks a label to two lines and never overflows into overflow-x: clip.
   Measured at 320 with the admin link present: 277px of 288px content box. */
.nav-inner {
  max-width: var(--measure);
  height: 100%;
  margin-inline: auto;
  padding-inline: clamp(var(--space-md), 4vw, var(--space-xl));
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-sm);
}

.brand {
  /* Never squeezed and never wrapped: the wordmark is the fixed point the rest
     of the rail is measured against. */
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: var(--tracking-display);
  line-height: 1;
  color: var(--color-ink);
  text-decoration: none;
  white-space: nowrap;
}

.brand svg {
  width: 1.15em;
  height: 1.15em;
  color: var(--color-accent);
}

/* margin-inline-start: auto would let the rail touch the wordmark once the
   viewport runs out; a real gap column cannot be collapsed by the layout. */
.nav-links {
  flex: none;
  margin-inline-start: auto;
  padding-inline-start: var(--space-md);
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-sm);
  list-style: none;
}

.nav-links form {
  display: flex;
  margin: 0;
}

.nav-links a {
  color: var(--color-ink-2);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 6px;
}

/* The rail's own button is the compact one: with a mouse a 32px button in a
   56px bar is the right density. A finger gets the 44px floor instead - the
   bar is 56px tall, so the target fits without changing the rail. */
.nav-links .btn {
  min-height: calc(var(--nav-h) - var(--space-lg));
  padding-inline: var(--space-sm);
  font-size: var(--text-sm);
}

@media (pointer: coarse) {
  .nav-links .btn {
    min-height: var(--control-h);
  }
}

/* Who you are signed in as: not a destination, so it is the first thing to go.
   When it is shown it is one line, clipped with an ellipsis, never wrapped -
   a wrapped name is what pushed the 56px bar out of shape. */
.nav-user,
.nav-links > .muted {
  display: none;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 14ch;
  /* Not --color-muted. The nav's ground is 86% paper over the page, and muted
     text on that computed 4.39:1 in dark - under the floor for 12.8px text,
     which is what this is. Mixed a third of the way to --color-ink-2 it clears
     it in both schemes and is still the quietest item in the rail. */
  color: color-mix(in oklab, var(--color-muted) 66%, var(--color-ink-2));
  font-size: var(--text-sm);
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (hover: hover) and (pointer: fine) {
  .nav-links a:hover {
    color: var(--color-ink);
    text-decoration-color: var(--color-accent);
  }
}

.nav-links a:active {
  color: var(--color-ink);
  text-decoration-color: var(--color-accent);
}

.nav-links a[aria-current="page"] {
  color: var(--color-ink);
  text-decoration-color: var(--color-accent);
}

/* Above 30rem there is room for the name and for the wordmark at display size. */
@media (min-width: 30rem) {
  .nav-inner {
    gap: var(--space-md);
  }

  .brand {
    font-size: var(--text-md);
    gap: var(--space-xs);
  }

  .nav-links a {
    font-size: var(--text-base);
  }

  .nav-links .btn {
    padding-inline: var(--space-md);
    font-size: var(--text-base);
  }

  .nav-user,
  .nav-links > .muted {
    display: block;
  }
}

@media (min-width: 48rem) {
  .nav-user,
  .nav-links > .muted {
    max-width: 22ch;
  }
}

/* -------------------------------------------------------------- primitives */

.container {
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: clamp(var(--space-md), 4vw, var(--space-xl));
  padding-block: var(--space-xl) var(--space-3xl);
}

/* .stack is vertical flow, .row is horizontal. Both take --gap so a caller can
   tighten one instance without a new class. */
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--gap, var(--space-md));
  min-width: 0;
}

.row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap, var(--space-sm));
  min-width: 0;
}

/* minmax(0, 1fr) and min(100%, ...) together are what keep this one column at
   375px with no horizontal scroll, whatever sits in a cell. */
.grid {
  display: grid;
  gap: var(--gap, var(--space-lg));
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.page-head {
  /* Single column, always: a label and a heading never sit side by side. */
  display: block;
  margin-block-end: var(--space-xl);
}

.page-head .h1 {
  margin-block-end: var(--space-2xs);
}

.page-head p {
  max-width: var(--prose);
  color: var(--color-muted);
}

.page-head .row {
  margin-block-start: var(--space-md);
}

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

.card {
  background: var(--color-paper);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  padding: var(--space-lg);
  box-shadow: var(--shadow-1);
  min-width: 0;
}

/* A card inside a card is nesting with no meaning; the inner one loses its
   frame and keeps only the rule. */
.card .card {
  border: 0;
  border-block-start: 1px solid var(--color-rule);
  border-radius: 0;
  padding: var(--space-md) 0 0;
  box-shadow: none;
  background: none;
}

.card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block-end: var(--space-sm);
  margin-block-end: var(--space-lg);
  border-block-end: 1px solid var(--color-rule);
}

.card-head > :first-child {
  min-width: 0;
}

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--control-h);
  padding: 0 var(--space-md);
  border: 1px solid var(--color-rule-2);
  border-radius: var(--radius-sm);
  background: var(--color-paper);
  color: var(--color-ink);
  font-size: var(--text-base);
  font-weight: 500;
  line-height: 1;
  /* An affordance is a one-line object. Let the row wrap, never the label. */
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition-property: background-color, border-color, color, transform;
  transition-duration: var(--dur-1);
  transition-timing-function: var(--ease-out);
}

.btn svg {
  width: 1.05em;
  height: 1.05em;
}

@media (hover: hover) and (pointer: fine) {
  .btn:hover {
    background: var(--color-paper-2);
    border-color: var(--color-rule-2);
  }
}

.btn:active {
  transform: translateY(1px);
}

.btn:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

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

/* Selected. A button in a segmented control - the preview widths, the effort
   switch - is a mode, and aria-pressed was being written by the live layer and
   styled nowhere, so the four width buttons stayed pixel-identical whichever
   one was on. Same recipe as .badge-admin: tint, ruled edge, accent ink, three
   channels rather than colour alone, and the pressed button says its own name
   in the label as well. */
.btn[aria-pressed="true"] {
  background: var(--color-accent-soft);
  border-color: color-mix(in oklab, var(--color-accent) 45%, transparent);
  color: color-mix(in oklab, var(--color-accent) 72%, var(--color-ink));
}

@media (hover: hover) and (pointer: fine) {
  .btn[aria-pressed="true"]:hover {
    background: color-mix(in oklab, var(--color-accent-soft) 88%, var(--color-ink));
    border-color: color-mix(in oklab, var(--color-accent) 60%, transparent);
  }
}

/* Loading, error and success are the three states an app button reaches that a
   pseudo-class cannot express. The label stays readable in all three. */
.btn[data-state="loading"] {
  cursor: progress;
}

.btn[data-state="loading"]::before {
  content: "";
  width: 0.9em;
  height: 0.9em;
  border: 2px solid currentColor;
  border-block-start-color: transparent;
  border-radius: var(--radius-pill);
  animation: kopet-spin 700ms linear infinite;
}

.btn[data-state="error"] {
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.btn[data-state="success"] {
  border-color: var(--color-ok);
  color: var(--color-ok);
}

@keyframes kopet-spin {
  to {
    transform: rotate(1turn);
  }
}

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

@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover {
    background: color-mix(in oklab, var(--color-accent) 88%, var(--color-ink));
    border-color: color-mix(in oklab, var(--color-accent) 88%, var(--color-ink));
  }
}

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--color-muted);
}

@media (hover: hover) and (pointer: fine) {
  .btn-ghost:hover {
    background: var(--color-paper-2);
    border-color: transparent;
    color: var(--color-ink);
  }
}

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

@media (hover: hover) and (pointer: fine) {
  .btn-danger:hover {
    background: color-mix(in oklab, var(--color-danger) 88%, var(--color-ink));
    border-color: color-mix(in oklab, var(--color-danger) 88%, var(--color-ink));
  }
}

/* The filled .btn-danger is for the button that confirms a destruction, where
   it is the only thing on screen. A destructive action sitting in a toolbar
   beside the controls the user reaches for all day is this one: it reads as
   danger by colour and by word, and it does not outweigh Reload and Open. */
.btn-danger-outline {
  background: transparent;
  border-color: color-mix(in oklab, var(--color-danger) 50%, var(--color-rule-2));
  color: color-mix(in oklab, var(--color-danger) 82%, var(--color-ink));
}

@media (hover: hover) and (pointer: fine) {
  .btn-danger-outline:hover {
    background: var(--color-danger-soft);
    border-color: var(--color-danger);
  }
}

/* ------------------------------------------------------------------ fields */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
}

.field-label {
  font-size: var(--text-sm);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  line-height: 1.3;
  color: var(--color-muted);
}

/* .table-input and .table-select are the same control sized to a table cell
   rather than to a form column; everything else about them is shared, so they
   ride every rule below with .field-input. */
.field-input,
.table-input,
.table-select {
  width: 100%;
  min-height: var(--control-h);
  padding: var(--space-xs) var(--space-sm);
  background: var(--color-paper);
  color: var(--color-ink);
  /* 1px in every state. A border that thickens on focus moves the page. */
  border: 1px solid var(--color-rule-2);
  border-radius: var(--radius-sm);
  /* The ring's slot is reserved at rest, so activating it shifts nothing. */
  outline: 2px solid transparent;
  outline-offset: 1px;
  /* 16px, because anything smaller makes iOS zoom the page on focus. */
  font-size: var(--text-base);
  line-height: 1.4;
  transition-property: background-color, border-color;
  transition-duration: var(--dur-1);
  transition-timing-function: var(--ease-out);
}

textarea.field-input {
  min-height: 6.5rem;
  line-height: var(--lh-body);
}

select.field-input {
  cursor: pointer;
}

/* In a table cell the control is as wide as its content needs, not as wide as
   the row: a width: 100% input inside a wrapping row claims the whole track
   and pushes the Save button onto a line of its own. */
.table-input {
  flex: 1 1 12ch;
  width: auto;
  min-width: 0;
  max-width: 24ch;
}

.table-select {
  flex: none;
  width: auto;
  cursor: pointer;
}

.field-input::placeholder,
.table-input::placeholder {
  color: var(--color-muted);
}

@media (hover: hover) and (pointer: fine) {
  .field-input:hover,
  .table-input:hover,
  .table-select:hover {
    background: var(--color-paper-2);
  }
}

.field-input:focus-visible,
.table-input:focus-visible,
.table-select:focus-visible {
  outline-color: var(--color-focus);
  border-color: var(--color-ink-2);
}

.field-input[aria-invalid="true"],
.table-input[aria-invalid="true"] {
  border-color: var(--color-danger);
  background: var(--color-danger-soft);
}

.field-input:disabled,
.table-input:disabled,
.table-select:disabled {
  opacity: 0.55;
  background: var(--color-paper-2);
  cursor: not-allowed;
}

/* The hint slot is always one line tall, empty or not, so an error appearing
   under a field does not push the form down - and a field with no hint at all
   still reserves the line, or the fields beside it sit at a different rhythm.
   The ::after is the empty case only, so the two never both apply. */
.field-hint {
  min-height: 1lh;
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--color-muted);
}

.field:not(:has(.field-hint))::after {
  content: "";
  display: block;
  min-height: 1lh;
  font-size: var(--text-sm);
  line-height: 1.4;
}

.field-hint[data-state="error"] {
  color: var(--color-danger);
}

/* One column on a narrow screen: the buttons go full width and stack in DOM
   order, and become a wrapping row once there is room for two of them. */
.form-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-sm);
  margin-block-start: var(--space-xs);
}

@media (min-width: 24rem) {
  .form-actions {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }
}

/* ------------------------------------------------------------------- table */

/* Mobile first, and on a phone this is not a table: every row becomes a card
   and every cell prints its own column head from data-label, so Status,
   Projects and Actions are on screen instead of parked in a silent horizontal
   scroll port where nothing hints they exist. Cells without data-label - a
   spacer, or a cell whose head is empty - just print their value.
   Markup contract: each <td> carries data-label="<its column head>". */
.table {
  display: block;
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.table thead {
  /* The heads are repeated into every card below; keeping the row as well
     would print each label twice. */
  display: none;
}

.table tbody {
  display: block;
}

.table tbody tr {
  display: block;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-paper);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  transition-property: background-color;
  transition-duration: var(--dur-1);
  transition-timing-function: var(--ease-out);
}

.table tbody tr + tr {
  margin-block-start: var(--space-sm);
}

.table th,
.table td {
  text-align: start;
}

.table th {
  font-size: var(--text-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
  white-space: nowrap;
}

.table td {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-2xs) var(--space-sm);
  padding-block: var(--space-2xs);
  border: 0;
}

/* Every level, not just the cell's direct children: these cells nest a row
   inside a form inside a row, and a flex item's automatic minimum size is its
   content's min-content width. One un-zeroed level in that chain is enough to
   push the whole subtree wider than the cell and out past the viewport. */
.table td *,
.table td > * {
  min-width: 0;
}

.table td[data-label]::before {
  content: attr(data-label);
  flex: 0 0 7rem;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
}

/* Hover is the mouse half of the same signal; :focus-within is the keyboard
   half, and the inline forms in these cells are all tab stops. */
@media (hover: hover) and (pointer: fine) {
  .table tbody tr:hover {
    background: var(--color-paper-2);
  }
}

.table tbody tr:focus-within {
  background: var(--color-paper-2);
}

/* Once there is room for every column, it is a table again. 64rem and not
   48rem: at 768 the cells are wide enough to be legal but not to be legible -
   every control in them stacks - and the card layout reads better there. */
@media (min-width: 64rem) {
  .table {
    display: table;
    /* Fixed layout, not auto: the cells hold inputs and three buttons, whose
       intrinsic width is wider than the card. With auto layout the table sizes
       itself to that content and spills out of the card and off the viewport,
       taking Enable and Delete with it - unreachable, and with no scroll port
       to reach them through. Fixed layout hands each column a share of the
       card instead, and the cells wrap inside it. */
    table-layout: fixed;
    width: 100%;
  }

  /* Cells are flex rows (above), so their controls wrap onto a second line
     rather than forcing the column wider than its share. */
  .table td {
    overflow-wrap: anywhere;
  }

  .table-input {
    max-width: 100%;
  }

  .table thead {
    display: table-header-group;
  }

  .table tbody {
    display: table-row-group;
  }

  .table tbody tr {
    display: table-row;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
  }

  .table tbody tr + tr {
    margin-block-start: 0;
  }

  .table th,
  .table td {
    display: table-cell;
    padding: var(--space-sm) var(--space-md);
    vertical-align: middle;
    border-block-end: 1px solid var(--color-rule);
  }

  .table th {
    border-block-end-color: var(--color-rule-2);
  }

  .table td[data-label]::before {
    content: none;
  }

  .table tbody tr:last-child td {
    border-block-end: 0;
  }

  /* The controls still wrap here. Pinning them to one line makes the cell's
     min-content width the sum of an input and three buttons, which is wider
     than the column's share of the card - and a fixed-layout table does not
     shrink to fit, it overflows, so Enable and Delete end up off the right
     edge of the viewport with no way to scroll to them. Wrapping keeps every
     control reachable and lets the row grow taller instead of wider. */
  .table td .row {
    flex-wrap: wrap;
  }

  @media (hover: hover) and (pointer: fine) {
    .table tbody tr:hover td {
      background: var(--color-paper-2);
    }
  }

  .table tbody tr:focus-within td {
    background: var(--color-paper-2);
  }
}

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

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-3xs) var(--space-xs);
  border: 1px solid var(--color-rule-2);
  border-radius: var(--radius-xs);
  background: var(--color-paper-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  white-space: nowrap;
}

/* Three roles, three weights of emphasis: admin carries the signal, developer
   is plain ink, viewer is an outline. Never colour alone - the badge says the
   word too. */
.badge-admin {
  background: var(--color-accent-soft);
  border-color: color-mix(in oklab, var(--color-accent) 40%, transparent);
  color: color-mix(in oklab, var(--color-accent) 72%, var(--color-ink));
}

.badge-developer {
  background: var(--color-paper-2);
  border-color: var(--color-rule-2);
  color: var(--color-ink-2);
}

.badge-viewer {
  background: transparent;
  border-color: var(--color-rule);
  color: var(--color-muted);
}

/* -------------------------------------------------------------------- bars */

/* Full hairline frame plus a tint. Never a thick coloured stripe on one edge. */
.error-bar,
.ok-bar {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  line-height: 1.45;
}

.error-bar {
  background: var(--color-danger-soft);
  border-color: color-mix(in oklab, var(--color-danger) 40%, transparent);
  color: color-mix(in oklab, var(--color-danger) 76%, var(--color-ink));
}

.ok-bar {
  background: var(--color-ok-soft);
  border-color: color-mix(in oklab, var(--color-ok) 40%, transparent);
  color: color-mix(in oklab, var(--color-ok) 76%, var(--color-ink));
}

.error-bar svg,
.ok-bar svg {
  width: 1.1em;
  height: 1.1em;
  margin-block-start: 0.2em;
}

/* ------------------------------------------------------------- empty state */

.empty {
  padding: var(--space-2xl) var(--space-lg);
  border: 1px dashed var(--color-rule-2);
  border-radius: var(--radius-md);
  color: var(--color-muted);
  text-align: start;
}

.empty .h2 {
  margin-block-end: var(--space-2xs);
  font-size: var(--text-md);
}

.empty p {
  max-width: var(--prose);
}

.empty .row {
  margin-block-start: var(--space-md);
}

/* ------------------------------------------------------------ project list */

.project-list {
  list-style: none;
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  /* clip so the first and last row hover fills stay inside the radius. */
  overflow: clip;
  background: var(--color-paper);
}

.project-item {
  /* The whole row is the link, not just the title: the title alone was a
     164x31 target in an 87px row at 320, so most of a row a finger aims at did
     nothing. The anchor stays the only tab stop and the only thing a screen
     reader reads; a stretched pseudo-element is what makes the row's area
     clickable, which is why this needs to be the positioning parent. */
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2xs) var(--space-md);
  padding: var(--space-md);
  border-block-start: 1px solid var(--color-rule);
  transition-property: background-color;
  transition-duration: var(--dur-1);
  transition-timing-function: var(--ease-out);
}

.project-list > .project-item:first-child {
  border-block-start: 0;
}

.project-item a {
  color: var(--color-ink);
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: var(--tracking-display);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.project-item a::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* With the row as the target the focus ring belongs around the row too. Both
   halves are behind @supports: without :has() the ring around the title is
   still the only ring there is, and dropping it would leave the keyboard with
   nothing to follow. */
@supports selector(:has(*)) {
  .project-item a:focus-visible {
    outline: none;
  }

  .project-item:has(a:focus-visible) {
    outline: 2px solid var(--color-focus);
    outline-offset: -2px;
  }
}

.project-item .muted {
  grid-column: 1;
  font-size: var(--text-sm);
  font-family: var(--font-mono);
}

@media (hover: hover) and (pointer: fine) {
  .project-item:hover {
    background: var(--color-paper-2);
  }
}

.project-item:focus-within {
  background: var(--color-paper-2);
}

/* ------------------------------------------------------------- status dots */

/* The dot is a second channel, never the only one: the markup beside it says
   "running" in words, and the dot carries aria-hidden. */
.dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--color-muted);
  /* The ring keeps the dot legible on both paper steps. */
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-muted) 18%, transparent);
}

.dot-running {
  background: var(--color-ok);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-ok) 20%, transparent);
}

/* Starting is a state that ends by itself, so it is warned about, not alarmed
   about. */
.dot-starting {
  background: var(--color-warn);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-warn) 20%, transparent);
}

.dot-crashed {
  background: var(--color-danger);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-danger) 20%, transparent);
}

.dot-stopped {
  background: var(--color-muted);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-muted) 18%, transparent);
}

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

.login-shell {
  /* Not a centred hero: the card sits on the upper third, left of nothing, and
     the page is the height of its content. */
  min-height: 100dvh;
  display: grid;
  align-content: start;
  justify-items: center;
  gap: var(--space-lg);
  padding: clamp(var(--space-2xl), 12vh, var(--space-3xl)) var(--space-md) var(--space-2xl);
}

.login-shell .brand {
  font-size: var(--text-lg);
}

.login-card {
  width: 100%;
  max-width: 24rem;
  padding: var(--space-lg);
  background: var(--color-paper);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}

.login-card .h1 {
  font-size: var(--text-lg);
  margin-block-end: var(--space-lg);
}

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

@media (min-width: 24rem) {
  .login-card {
    padding: var(--space-xl);
  }
}

/* --------------------------------------------------- project page: toolbar */

/* Docked under the nav, never at top: 0 - two sticky bars at the same offset
   overlap and the deeper one paints over the nav. */
.toolbar {
  position: sticky;
  inset-block-start: var(--nav-h);
  z-index: var(--z-sticky);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  background: var(--color-paper);
  border-block-end: 1px solid var(--color-rule);
}

/* One height for every control on the page. The toolbar used to run a step
   shorter, which put ten 32px buttons - 36px even on a touch screen - beside
   40px controls in the drawer: under the 44px hit-target floor and out of
   rhythm with the rest of the page at the same time. The smaller label is all
   the density this zone gets. */
.toolbar .btn {
  padding: 0 var(--space-xs);
  font-size: var(--text-sm);
  /* Icon-only until there is room for words, so the phone toolbar is one row
     of square targets instead of three rows of labelled ones. */
  min-width: var(--control-h);
}

.toolbar .row {
  --gap: var(--space-2xs);
}

/* The trailing group. Not :last-child: the live layer appends #frame-notice to
   the toolbar, and from that moment :last-child matches the notice instead and
   the group silently loses its trailing edge. */
.toolbar-end {
  margin-inline-start: auto;
}

/* Below the two-pane breakpoint the name takes the first line to itself and
   the controls take the second, so the toolbar is two predictable rows however
   long the project's name is, and the row of controls reads as one row.
   Reload sits at the leading edge of it and the drawer and app actions at the
   trailing edge. */
.toolbar-identity {
  flex: 1 1 100%;
}

.toolbar-identity strong {
  overflow-wrap: anywhere;
  min-width: 0;
}

/* Choosing a 1280 preview inside a 375 viewport is a control that cannot do
   anything, so the width group only exists where the stage is wide enough for
   the widths to differ. */
.toolbar-widths {
  display: none;
}

.toolbar .btn-label {
  display: none;
}

/* The path the dev app reports over postMessage. Alone it renders as a stray
   "/" floating between two buttons; keyed and boxed it reads as the address of
   what is on the stage. It is the first thing to go when the row is short. */
.path-chip {
  display: none;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--control-h);
  max-width: 24ch;
  padding: 0 var(--space-sm);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-sm);
  background: var(--color-paper-2);
}

.path-chip-key {
  flex: none;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
}

.path-chip .mono {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

@media (min-width: 40rem) {
  .toolbar .btn {
    min-width: 0;
    padding: 0 var(--space-sm);
  }

  .toolbar .btn-label {
    display: inline;
  }
}

@media (min-width: 48rem) {
  .path-chip {
    display: inline-flex;
  }
}

@media (min-width: 60rem) {
  .toolbar-identity {
    flex: 0 1 auto;
  }

  .toolbar-widths {
    display: flex;
  }
}

/* ----------------------------------------------------- project page: split */

/* The toolbar, the stage and the chat column are one workbench that owns the
   space under the nav. A flex column is what lets the desktop layout subtract
   the toolbar's real height from the viewport instead of guessing at it - the
   toolbar is sticky, so a split sized at 100dvh - nav overhangs by exactly the
   toolbar and the page scrolls for no reason. */
.workbench {
  display: flex;
  flex-direction: column;
  min-height: calc(100dvh - var(--nav-h));
}

/* Phone first, and the phone is not two panes stacked. The stage comes first
   in the markup and takes the whole first screen; the chat column sits under
   it, one scroll away. A two-pane builder cannot be two panes at 375, and the
   pane with no content in it until Phase 2 is the last thing that should hold
   the fold. Above 60rem the same two elements are placed as the real two
   columns - chat on the leading edge - which is why the grid places them by
   column rather than by source order. */
.split {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  min-height: 0;
}

/* Two elements, not one, and the reason is the drawer. The drawer and its
   scrim are absolutely positioned against .stage, and a closed drawer is
   `inset-inline-end: 0; transform: translateX(100%)` - a box translated past
   the trailing edge, which still counts as scrollable overflow. When .stage
   was also the scroll box, a 900px stage with no drawer open reported
   scrollWidth 1444 and any sideways trackpad swipe dragged the preview half
   off the leading edge to reveal 544px of bare paper. So .stage stays the
   positioning parent and clips, and the scrolling the width buttons need -
   a 1280 preview inside a 900px stage has to be reachable - happens one
   element in, where no drawer is a child. */
.stage {
  position: relative;
  min-width: 0;
  /* The subject of the page owns the first screen: everything above it here is
     the nav and the toolbar, and this is the rest. */
  min-height: calc(100dvh - var(--nav-h) - var(--toolbar-h));
  background: var(--color-paper-3);
  display: grid;
  overflow: clip;
}

/* The frame's own box: what frame.js measures for the width buttons and what
   scrolls when the chosen width is wider than the stage. */
.stage-scroll {
  min-width: 0;
  min-height: 0;
  padding: var(--space-xs);
  display: grid;
  justify-items: center;
  align-items: stretch;
  overflow: clip;
}

.frame-wrap {
  /* The toolbar width buttons set --frame-w; fit means no cap at all. */
  width: 100%;
  max-width: var(--frame-w, 100%);
  min-height: 0;
  background: var(--color-paper);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  /* No transition on max-width: animating a layout property janks the frame,
     and the width buttons want an instant answer anyway. */
  overflow: clip;
}

.frame-wrap > iframe {
  width: 100%;
  height: 100%;
  min-height: 60dvh;
  border: 0;
  background: var(--color-paper);
}

/* ------------------------------------------------ project page: chat panel */

/* The reserved Phase 2 column. It is a real panel with a solid frame, not the
   dashed .empty box that says "under construction", and it fills its column:
   the note sits at the top of the message area and the composer holds the
   bottom edge, so the shape Phase 2 fills in is already on screen. Phase 2
   replaces what is inside .chat-log and .chat-composer and changes no layout. */
.chat-panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background: var(--color-paper);
  /* On a phone the chat sits under the stage, so its rule is on top. */
  border-block-start: 1px solid var(--color-rule);
}

.chat-log {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--space-md);
  overflow: auto;
}

.chat-note {
  padding: var(--space-md);
  background: var(--color-paper-2);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  color: var(--color-muted);
}

.chat-note .h2 {
  margin-block-end: var(--space-2xs);
  font-size: var(--text-md);
}

.chat-note p {
  max-width: var(--prose);
}

.chat-note p + p {
  margin-block-start: var(--space-xs);
}

/* The composer is inert until Phase 2 and says so above; what it is doing here
   is holding its own height, so the column has its real proportions now and
   the chat does not arrive by moving everything. */
.chat-composer {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-block-start: 1px solid var(--color-rule);
  background: var(--color-paper);
}

.chat-composer textarea.field-input {
  min-height: 4.5rem;
}

.chat-composer .row {
  justify-content: space-between;
}

/* Four controls on one line of a 380px column: the effort switch reads as a
   set of small options, and Send is the one full-size affordance. */
.chat-composer .btn {
  padding: 0 var(--space-sm);
  font-size: var(--text-sm);
}

.chat-composer p {
  font-size: var(--text-sm);
}

/* Two columns once there is room for a 380px panel and a usable frame beside
   it. The columns are placed explicitly because the markup is stage first. */
@media (min-width: 60rem) {
  .workbench {
    height: calc(100dvh - var(--nav-h));
    min-height: 0;
    overflow: clip;
  }

  .split {
    grid-template-columns: minmax(0, var(--chat-w)) minmax(0, 1fr);
  }

  .chat-panel {
    grid-column: 1;
    grid-row: 1;
    border-block-start: 0;
    border-inline-end: 1px solid var(--color-rule);
  }

  .stage {
    grid-column: 2;
    grid-row: 1;
    min-height: 0;
  }

  .stage-scroll {
    padding: var(--space-md);
    overflow: auto;
  }

  .frame-wrap > iframe {
    min-height: 0;
  }
}

/* ---------------------------------------------------- project page: drawer */

/* Files, logs, versions: one panel that slides over the stage from the trailing
   edge. Closed is the default so the markup can ship it inert. */
.drawer {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  z-index: var(--z-drawer);
  width: min(100%, 34rem);
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--color-paper);
  /* The boundary against a live page, not against paper: --color-rule is
     invisible over the preview and the panel's edge then reads as a tear. */
  border-inline-start: 1px solid var(--color-rule-2);
  box-shadow: var(--shadow-2);
  transform: translateX(100%);
  visibility: hidden;
  transition-property: transform, visibility;
  transition-duration: var(--dur-2);
  transition-timing-function: var(--ease-out);
}

.drawer[data-open] {
  transform: none;
  visibility: visible;
}

.drawer > .card-head {
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  border-block-end: 1px solid var(--color-rule);
}

.drawer > pre,
.drawer > .stack {
  margin: 0;
  padding: var(--space-md);
  overflow: auto;
  min-height: 0;
  flex: 1;
}

/* An icon-only close button is still a target, so it is square. */
.drawer [data-drawer-close] {
  min-width: var(--control-h);
  padding-inline: var(--space-2xs);
}

/* The drawer is a layer over the preview and the preview has to look like it
   went behind something. Without this the panel's straight edge cuts the page
   underneath mid-word and reads as a paint glitch, and the only ways out are
   Escape and the x. The scrim carries data-drawer-close, so a click anywhere
   outside the panel dismisses it through the handler the x already uses. */
.scrim {
  position: absolute;
  inset: 0;
  z-index: calc(var(--z-drawer) - 1);
  background: color-mix(in oklab, var(--color-ink) 38%, transparent);
  opacity: 0;
  visibility: hidden;
  cursor: pointer;
  transition-property: opacity, visibility;
  transition-duration: var(--dur-2);
  transition-timing-function: var(--ease-out);
}

.drawer[data-open] + .scrim {
  opacity: 1;
  visibility: visible;
}

/* --------------------------------------------------------- reduced motion */

/* Spatial motion collapses; nothing that carries meaning disappears. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-1: 0ms;
    --dur-2: 0ms;
  }

  .drawer,
  .scrim {
    transition: none;
  }

  .btn:active {
    transform: none;
  }

  .btn[data-state="loading"]::before {
    animation: none;
    border-block-start-color: currentColor;
    opacity: 0.5;
  }
}
