/* ═══════════════════════════════════════════════════════════════
   LaikaSuite — Component layer
   Every interactive primitive in the suite. All five applications
   compose their interface exclusively from these classes so a control
   behaves and reads identically wherever a user meets it.

   Naming: .ls-<block>__<element> --<modifier via data attribute>
   ═══════════════════════════════════════════════════════════════ */

/* ═══ Glass surface — the system's signature primitive ═════════

   Three declarations and nothing else: the tint, the lens, the edge. Any
   surface in the suite that wants to be glass wears .ls-glass or repeats
   exactly these three — a pane that blurs without lifting saturation is a
   sheet of frosted plastic, and a pane without its lit top edge and
   shaded underside is a rectangle of translucent paint. */
.ls-glass {
  background: var(--glass);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  border: var(--rule-strong) solid var(--border-ink);
  box-shadow: var(--shadow-md), var(--shadow-sheen);
}
.ls-glass[data-weight="strong"] {
  background: var(--glass-strong);
  backdrop-filter: var(--glass-filter-heavy);
  -webkit-backdrop-filter: var(--glass-filter-heavy);
}
.ls-glass[data-weight="subtle"] { background: var(--glass-subtle); box-shadow: var(--shadow-sm), var(--shadow-sheen); }

/* Panel — a glass card with standard padding. */
.ls-panel {
  padding: var(--space-5);
  background: var(--glass);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  border: var(--rule-strong) solid var(--border-ink);
  box-shadow: var(--shadow-md), var(--shadow-sheen);
}

/* ── Where there is no lens ──────────────────────────────────
   Without backdrop-filter a 62%-opaque pane is a window onto whatever
   happens to be behind it, and the technical labels on a toolbar stop
   being readable. Every glass surface in the suite is named here — the
   list has to be complete, because a surface missing from it is a surface
   that goes transparent on the browsers that need this most. The tint is
   the same colour the pane carries; only the alpha goes to 1. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .ls-glass, .ls-glass[data-weight="strong"], .ls-glass[data-weight="subtle"],
  .ls-panel, .ls-menu, .ls-dialog, .ls-toast, .ls-topbar, .ls-toolbar,
  .ls-rail, .ls-statusbar, .bar, .bar-control, .burger, .menu-overlay,
  .directive, .stamp, .languages-panel, .ls-btn[data-variant="secondary"] {
    background: rgb(var(--pane-rgb));
  }
  /* The chrome rows sit directly against each other, so they take the
     sheet rather than the card and keep their separation. */
  .ls-toolbar, .ls-rail, .ls-statusbar, .menu-overlay {
    background: var(--surface-sunken);
  }
  .ls-dialog::backdrop { background: var(--scrim); }
}

/* ═══════════════════════════════════════════════════════════════
   THE POSTER VOICE — display type

   Anton, uppercase, tight. It is used for headlines, wordmarks and the
   title of an empty state, and for nothing else: the moment it appears in
   a toolbar it stops being a voice and becomes noise.

   It comes in two forms, and the difference is not cosmetic:

     .ls-display   a *headline*. It is translated, so in a script Anton
                   cannot set it hands over to a heavy system face and
                   stops uppercasing (see the language rules below).
     .ls-wordmark  a *name*. "LaikaSuite", "Write", "Vault" — Latin in all
                   23 locales, because a product name is not translated.
                   It therefore keeps Anton in every language, and an
                   Arabic or Japanese interface still carries the same
                   wordmark as an English one.

   Elements whose markup is generated rather than authored are named here
   so they get the right one without an application having to know.
   ═══════════════════════════════════════════════════════════════ */
.ls-display,
.ls-wordmark,
.ls-empty__title,
.ls-dialog__title,
.ls-intro__title,
.ls-about__name,
.ls-brand__app {
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  text-transform: uppercase;
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-display);
  /* Anton has no italic and no second weight; asking for one produces a
     slanted or smeared synthetic face. */
  font-synthesis: none;
}

/* Scripts Anton cannot set (see tokens.css) borrow a heavy system face.
   Those faces are not condensed, so the same nominal size sets perhaps a
   third wider — the display ramp steps down with them. Their marks also
   stack higher and hang lower than Latin capitals, so the leading opens
   up, and uppercasing is meaningless — or actively destructive — outside
   the Latin, Cyrillic and Greek alphabets.

   Wordmarks are excluded on purpose: they are Latin in every locale. */
:root:is(:lang(pl), :lang(cs), :lang(hu), :lang(ro), :lang(tr), :lang(vi),
  :lang(ru), :lang(uk), :lang(el), :lang(zh), :lang(ja), :lang(ko),
  :lang(ar), :lang(hi), :lang(th))
  :is(h1, h2, h3, .ls-display, .ls-empty__title, .ls-dialog__title, .ls-intro__title) {
  /* Declared on the element, not on :root, so a wordmark sitting in the
     same document keeps Anton's own ramp. */
  --display-lg: clamp(2rem, 1.05rem + 4vw, 3.5rem);
  --display-md: clamp(1.5rem, 1.2rem + 1.4vw, 2.125rem);
  --display-sm: clamp(1.125rem, 1.03rem + 0.42vw, 1.375rem);
  --display-xs: 1rem;
}
:root:is(:lang(ar), :lang(hi), :lang(th))
  :is(h1, h2, h3, .ls-display, .ls-empty__title, .ls-dialog__title, .ls-intro__title) {
  line-height: 1.34;
  text-transform: none;
  letter-spacing: 0;
}
:root:is(:lang(zh), :lang(ja), :lang(ko))
  :is(h1, h2, h3, .ls-display, .ls-empty__title, .ls-dialog__title, .ls-intro__title) {
  line-height: 1.2;
  letter-spacing: 0;
}
/* A wordmark never leaves Anton, so it never leaves Anton's metrics. */
.ls-wordmark, .ls-about__name, .ls-brand__app {
  --font-display: "LS Display", "Arial Black", Impact, sans-serif;
  --font-display-weight: 400;
}

.ls-display[data-size="lg"] { font-size: var(--display-lg); }
.ls-display[data-size="md"] { font-size: var(--display-md); }
.ls-display[data-size="sm"] { font-size: var(--display-sm); }
.ls-display[data-size="xs"] { font-size: var(--display-xs); }

/* ═══════════════════════════════════════════════════════════════
   THE READING VOICE — serif prose

   PT Serif, the same face and the same file the company site sets its
   body copy in. It marks the difference between text you *read* and text
   you *operate*: an explanation, an empty state, an About panel. Never a
   label, never a control.
   ═══════════════════════════════════════════════════════════════ */
.ls-prose,
.ls-empty__text,
.ls-library__empty,
.ls-intro__body,
.ls-about__tagline {
  font-family: var(--font-serif);
  line-height: var(--leading-loose);
}

/* ═══════════════════════════════════════════════════════════════
   THE INSTRUMENT VOICE — technical labels

   A monospace caption, uppercase and widely tracked: the annotation on a
   technical drawing. Everything that names a region rather than saying
   something wears it, which is what gives eight applications the same
   diagram seriousness without a single extra element.
   ═══════════════════════════════════════════════════════════════ */
.ls-label,
.ls-rail__title,
.ls-rail__section-title,
.ls-menu__heading,
.ls-shortcuts__group,
.ls-brand__suite {
  font-family: var(--font-mono);
  /* astralaika.com's .mono, to the decimal: 12.5px at 0.12em, uppercase. */
  font-size: var(--text-label);
  font-weight: var(--weight-regular);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  /* Muted, not faint. These labels name the regions of an interface and a
     user has to be able to read them: --text-faint is drawn at 3:1 and
     that is a decoration, not a word. --text-muted is 4.8:1 in both
     themes — the same on either side of the day. */
  color: var(--text-muted);
}
/* PT Mono covers Latin and Cyrillic. In the scripts it does not, a
   *translated* label would fall back to whatever monospace the system has
   for that script, tracked at 0.14em and pointlessly uppercased — so
   those keep the interface face instead.

   Labels that are Latin in every locale — a wordmark's eyebrow, a plate
   number, a list of file formats — say so with data-latin and keep the
   instrument face throughout. .ls-brand__suite is one of them by name. */
:root:lang(zh) :is(.ls-label:not([data-latin]), .ls-rail__title, .ls-rail__section-title, .ls-menu__heading, .ls-shortcuts__group),
:root:lang(ja) :is(.ls-label:not([data-latin]), .ls-rail__title, .ls-rail__section-title, .ls-menu__heading, .ls-shortcuts__group),
:root:lang(ko) :is(.ls-label:not([data-latin]), .ls-rail__title, .ls-rail__section-title, .ls-menu__heading, .ls-shortcuts__group),
:root:lang(ar) :is(.ls-label:not([data-latin]), .ls-rail__title, .ls-rail__section-title, .ls-menu__heading, .ls-shortcuts__group),
:root:lang(hi) :is(.ls-label:not([data-latin]), .ls-rail__title, .ls-rail__section-title, .ls-menu__heading, .ls-shortcuts__group),
:root:lang(th) :is(.ls-label:not([data-latin]), .ls-rail__title, .ls-rail__section-title, .ls-menu__heading, .ls-shortcuts__group) {
  font-family: var(--font-sans);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-caps);
  text-transform: none;
}

/* The label, annotated: the caption and the rule it captions, the way a
   drawing marks out a region. The rule takes the rest of the line. */
.ls-label[data-rule],
.ls-rail__title,
.ls-rail__section-title,
.ls-shortcuts__group,
.ls-menu__heading {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.ls-label[data-rule]::after,
.ls-rail__title::after,
.ls-rail__section-title::after,
.ls-shortcuts__group::after,
.ls-menu__heading::after {
  content: "";
  flex: 1;
  min-width: var(--space-4);
  height: var(--rule);
  background: var(--border);
}

/* ═══════════════════════════════════════════════════════════════
   THE RECURRING FORMS

   The orbit, the trajectory, the star, the rule and the node. Every one
   of them is drawn as a *mask* over a painted box, so the shape takes the
   colour of whatever it is placed in — one file serves both themes, all
   eight product accents and any disabled state.

   Size them with the custom property, not with width/height, so the
   aspect ratio can never be broken by accident.
   ═══════════════════════════════════════════════════════════════ */
.ls-star,
.ls-orbit,
.ls-halo,
.ls-trajectory {
  display: inline-block;
  flex: none;
  background: currentColor;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}

/* The star — four-pointed, the "astra" in the name. Sized in em so it can
   sit inside a line of type as a piece of punctuation. */
.ls-star {
  width: var(--form-size, 1em);
  height: var(--form-size, 1em);
  -webkit-mask-image: var(--shape-star);
  mask-image: var(--shape-star);
}

/* The orbit — a ring, broken at the top right. */
.ls-orbit {
  width: var(--form-size, 72px);
  height: var(--form-size, 72px);
  -webkit-mask-image: var(--shape-orbit);
  mask-image: var(--shape-orbit);
}

/* The whole mark: the orbit with its star in the gap. */
.ls-halo {
  width: var(--form-size, 72px);
  height: var(--form-size, 72px);
  -webkit-mask-image: var(--shape-halo);
  mask-image: var(--shape-halo);
}

/* The trajectory — one arc, a node at each end. */
.ls-trajectory {
  width: var(--form-size, 120px);
  aspect-ratio: 2 / 1;
  -webkit-mask-image: var(--shape-trajectory);
  mask-image: var(--shape-trajectory);
}

/* Windows high contrast repaints backgrounds with the system palette,
   which would erase a shape that *is* its background. These opt out so
   the mark survives the substitution. */
@media (forced-colors: active) {
  .ls-star, .ls-orbit, .ls-halo, .ls-trajectory, .ls-node, .ls-spinner,
  .ls-empty__art::before, .ls-rule {
    forced-color-adjust: none;
    background: CanvasText;
  }
}

/* The node — a filled circle where two things meet. */
.ls-node {
  display: inline-block;
  flex: none;
  width: var(--form-size, 6px);
  height: var(--form-size, 6px);
  background: currentColor;
  border-radius: var(--radius-full);
}

/* The rule — a technical hairline. With ticks it becomes a registration
   mark: the line knows where it starts and where it stops. */
.ls-rule {
  position: relative;
  flex: 1;
  min-width: 0;
  height: var(--rule);
  border: 0;
  background: var(--border);
}
.ls-rule[data-weight="strong"] { height: var(--rule-strong); background: var(--border-strong); }
.ls-rule[data-ticks]::before,
.ls-rule[data-ticks]::after {
  content: "";
  position: absolute;
  top: calc(var(--tick) * -1);
  width: var(--rule);
  height: calc(var(--tick) * 2 + var(--rule));
  background: inherit;
}
.ls-rule[data-ticks]::before { inset-inline-start: 0; }
.ls-rule[data-ticks]::after  { inset-inline-end: 0; }

/* An orbit slowly turning: the suite's loading state and the quiet motion
   behind a hero. One rotation is nine seconds — a spacecraft, not a
   spinner. Reduced motion stops it dead; the shape still reads.

   Scoped to the forms rather than declared on a bare [data-spin]: a
   design system should not claim a global attribute name. */
@keyframes ls-orbit-spin { to { rotate: 360deg; } }
:is(.ls-star, .ls-orbit, .ls-halo, .ls-trajectory)[data-spin] {
  animation: ls-orbit-spin 9s linear infinite;
}
:is(.ls-star, .ls-orbit, .ls-halo, .ls-trajectory)[data-spin="fast"] {
  animation-duration: 2.4s;
}
@media (prefers-reduced-motion: reduce) {
  :is(.ls-star, .ls-orbit, .ls-halo, .ls-trajectory)[data-spin] { animation: none; }
}

/* ═══ Button — the notched constructivist plate ════════════════

   astralaika.com's .btn, taken whole: Anton at 14px tracked 0.06em and
   uppercased, 13px by 26px of padding, square, and with the top-right
   corner clipped off at fourteen pixels. On hover the plate steps two
   pixels up and to the left and a hard square of ink appears underneath
   it; pressing it puts the plate back down and the shadow goes out.

   The one thing that is the suite's rather than the site's is the fill of
   the secondary plate, which is glass instead of nothing.

   The notch is a clip-path, and a clip-path takes the outline with it —
   so focus is drawn *inside* the plate, as a ring the clip cannot reach.
   A focus indicator that the shape deletes is worse than no shape. */
.ls-btn {
  --btn-bg: transparent;
  --btn-fg: var(--text);
  --btn-border: transparent;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  padding: 0 var(--space-5);
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-size: var(--text-action);
  letter-spacing: var(--tracking-action);
  text-transform: uppercase;
  font-synthesis: none;
  line-height: 1;
  white-space: nowrap;
  color: var(--btn-fg);
  background: var(--btn-bg);
  border: var(--rule-strong) solid var(--btn-border);
  transition: transform 0.12s var(--ease-out), box-shadow 0.12s var(--ease-out),
    background-color 0.15s var(--ease-out), color 0.15s var(--ease-out),
    border-color 0.15s var(--ease-out);
  user-select: none;
}
.ls-btn:hover:not(:disabled) { background: var(--hover); }
.ls-btn .ls-icon { flex: none; }
/* Inside, because the notch clips anything drawn outside the plate. */
.ls-btn:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 var(--rule-strong) var(--accent),
    inset 0 0 0 calc(var(--rule-strong) * 2) var(--accent-contrast);
}
/* Anything the company would print on a plate is clipped like one. Ghost
   buttons are not plates — they are words in a toolbar — so they keep
   their corners and their outline. */
.ls-btn:not([data-variant="ghost"]) { clip-path: var(--notch); }
[dir="rtl"] .ls-btn:not([data-variant="ghost"]) {
  clip-path: polygon(14px 0, 100% 0, 100% 100%, 0 100%, 0 14px);
}

/* Primary — the one action a surface is asking for. The company's
   .btn-red: solid, no border, and it goes darker as it steps up. */
.ls-btn[data-variant="primary"] {
  --btn-fg: var(--accent-contrast);
  --btn-bg: var(--accent);
  --btn-border: transparent;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.22);
}
.ls-btn[data-variant="primary"]:hover:not(:disabled) {
  --btn-bg: var(--accent-hover);
  transform: translate(calc(-2px * var(--shadow-x)), -2px);
  box-shadow: var(--shadow-hard), inset 0 1px 0 rgb(255 255 255 / 0.22);
}

/* Secondary — the company's .btn-line, whose fill is the suite's glass:
   two pixels of ink around a lensed pane. On hover the site fills the
   plate with ink and casts the shadow in red; here the ink is the theme's
   own and the shadow is the product's hue. */
.ls-btn[data-variant="secondary"] {
  --btn-bg: var(--glass-strong);
  --btn-border: var(--border-ink);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  box-shadow: var(--shadow-sheen);
}
.ls-btn[data-variant="secondary"]:hover:not(:disabled) {
  --btn-bg: var(--text);
  --btn-fg: var(--surface);
  transform: translate(calc(-2px * var(--shadow-x)), -2px);
  box-shadow: var(--shadow-hard-accent);
}

/* Ghost — the default for dense toolbars. */
.ls-btn[data-variant="ghost"] { --btn-fg: var(--text-secondary); }
.ls-btn[data-variant="ghost"]:hover:not(:disabled) { --btn-fg: var(--text); }

/* Danger — destructive confirmation only. */
.ls-btn[data-variant="danger"] {
  --btn-fg: #fff;
  --btn-bg: var(--ls-danger);
}
.ls-btn[data-variant="danger"]:hover:not(:disabled) {
  --btn-bg: #cf3438;
  transform: translate(calc(-2px * var(--shadow-x)), -2px);
  box-shadow: var(--shadow-hard);
}

/* Pressed: the plate goes back down on the page and the shadow goes out.
   The company's own `.btn:active { transform: translate(0,0); box-shadow: none }`. */
.ls-btn:active:not(:disabled) { transform: translate(0, 0); box-shadow: none; }

.ls-btn[data-size="sm"] { min-height: 28px; padding: 0 var(--space-3); font-size: var(--text-xs); }
.ls-btn[data-size="lg"] { min-height: 46px; padding: 13px var(--space-6); font-size: var(--text-lg); }
.ls-btn[data-block] { display: flex; width: 100%; }

/* Icon-only button — square, used across every toolbar. */
.ls-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--tap-target);
  height: var(--tap-target);
  color: var(--text-secondary);
  border-radius: var(--radius-md);
  transition: var(--transition-control);
}
.ls-icon-btn:hover:not(:disabled) { color: var(--text); background: var(--hover); }
.ls-icon-btn:active:not(:disabled) { transform: scale(0.93); }
/* Toggles report state with aria-pressed; members of a radiogroup (the
   Whiteboard tool bar) report it with aria-checked. Both look the same. */
.ls-icon-btn[aria-pressed="true"],
.ls-icon-btn[aria-checked="true"],
.ls-icon-btn[data-active="true"] {
  color: var(--accent);
  background: var(--accent-wash);
}
.ls-icon-btn[data-size="sm"] { width: 28px; height: 28px; border-radius: var(--radius-sm); }
.ls-icon-btn[data-size="lg"] { width: 40px; height: 40px; }

.ls-icon { width: 18px; height: 18px; stroke-width: 1.75; }
.ls-icon[data-size="sm"] { width: 15px; height: 15px; }
.ls-icon[data-size="lg"] { width: 22px; height: 22px; }

/* ═══ Field — input, textarea, select ═════════════════════════ */
.ls-field { display: flex; flex-direction: column; gap: var(--space-2); }
.ls-field__label {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
}
.ls-field__hint { font-size: var(--text-xs); color: var(--text-muted); }
.ls-field__error { font-size: var(--text-xs); color: var(--ls-danger); }

.ls-input,
.ls-textarea,
.ls-select {
  width: 100%;
  min-height: var(--tap-target);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--surface);
  /* astralaika.com's .lang-current: two pixels of ink around the card. */
  border: var(--rule-strong) solid var(--border-ink);
  transition: var(--transition-control);
}
[data-theme="dark"] .ls-input,
[data-theme="dark"] .ls-textarea,
[data-theme="dark"] .ls-select { background: var(--surface-inset); }

.ls-input::placeholder, .ls-textarea::placeholder { color: var(--text-faint); }
.ls-input:hover, .ls-textarea:hover, .ls-select:hover { border-color: var(--text-faint); }
.ls-input:focus-visible,
.ls-textarea:focus-visible,
.ls-select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}
.ls-textarea { min-height: 88px; line-height: var(--leading-body); resize: vertical; }

.ls-select {
  padding-inline-end: var(--space-8);
  appearance: none;
  /* Chevron drawn as a data URI so no network request and no icon font. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236b7a90' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6 4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 16px;
}
[dir="rtl"] .ls-select { background-position: left var(--space-3) center; }

/* Search field with a leading icon slot.
   The *holder* is positioned, not the icon: hydrateIcons injects the <svg>
   inside the placeholder element, so absolutely positioning the svg itself
   leaves it at the holder's static position — which, in a zero-height flex
   item, sits half an icon below the centre line. */
.ls-search { position: relative; display: flex; align-items: center; }
.ls-search > [aria-hidden],
.ls-search > .ls-icon {
  position: absolute;
  inset-inline-start: var(--space-3);
  top: 50%;
  translate: 0 -50%;
  display: flex;
  align-items: center;
  color: var(--text-faint);
  pointer-events: none;
}
.ls-search .ls-input { padding-inline-start: calc(var(--space-3) * 2 + 15px); }

/* ═══ Switch ══════════════════════════════════════════════════ */
.ls-switch {
  position: relative;
  flex: none;
  width: 38px;
  height: 22px;
  padding: 0;
  background: var(--border-strong);
  border-radius: var(--radius-full);
  transition: background-color var(--duration-normal) var(--ease-out);
}
.ls-switch::after {
  content: "";
  position: absolute;
  top: 3px;
  inset-inline-start: 3px;
  width: 16px;
  height: 16px;
  background: #fff;
  border-radius: var(--radius-full);
  box-shadow: var(--shadow-sm);
  transition: transform var(--duration-normal) var(--ease-spring);
}
.ls-switch[aria-checked="true"] { background: var(--accent); }
.ls-switch[aria-checked="true"]::after { transform: translateX(16px); }
[dir="rtl"] .ls-switch[aria-checked="true"]::after { transform: translateX(-16px); }

/* ═══ Segmented control ═══════════════════════════════════════ */
.ls-segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface-inset);
  border-radius: var(--radius-lg);
}
.ls-segmented > button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 28px;
  padding: 0 var(--space-3);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  border-radius: var(--radius-md);
  transition: var(--transition-control);
}
.ls-segmented > button:hover { color: var(--text); }
.ls-segmented > button[aria-checked="true"],
.ls-segmented > button[aria-selected="true"] {
  color: var(--text);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
}
[data-theme="dark"] .ls-segmented > button[aria-checked="true"],
[data-theme="dark"] .ls-segmented > button[aria-selected="true"] { background: var(--surface-raised); }

/* ═══ Menu (dropdown / context) ═══════════════════════════════ */
.ls-menu {
  position: fixed;
  z-index: var(--z-dropdown);
  min-width: 208px;
  max-width: min(320px, calc(100vw - var(--space-4) * 2));
  max-height: min(70vh, 520px);
  padding: var(--space-2);
  overflow-y: auto;
  background: var(--glass-strong);
  backdrop-filter: var(--glass-filter-heavy);
  -webkit-backdrop-filter: var(--glass-filter-heavy);
  /* astralaika.com's .lang-menu: two pixels of ink and a six-pixel
     offset square underneath. Its fill is the suite's glass. */
  border: var(--rule-strong) solid var(--border-ink);
  box-shadow: var(--shadow-md), var(--shadow-sheen);
  transform-origin: var(--menu-origin, top left);
  animation: ls-menu-in var(--duration-fast) var(--ease-out);
}
@keyframes ls-menu-in {
  from { opacity: 0; transform: scale(0.96) translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.ls-menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 32px;
  padding: 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--text);
  text-align: start;
  border-radius: var(--radius-sm);
  transition: background-color var(--duration-instant) var(--ease-out);
}
.ls-menu__item:hover:not(:disabled),
.ls-menu__item[data-highlighted="true"] { background: var(--hover); }
.ls-menu__item[aria-checked="true"], .ls-menu__item[aria-selected="true"] { color: var(--accent); }
.ls-menu__item .ls-icon { color: var(--text-muted); }
.ls-menu__item:hover .ls-icon { color: var(--text-secondary); }
.ls-menu__item[data-variant="danger"] { color: var(--ls-danger); }
.ls-menu__item[data-variant="danger"] .ls-icon { color: var(--ls-danger); }
.ls-menu__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ls-menu__shortcut {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-faint);
  letter-spacing: 0;
}
.ls-menu__separator { height: var(--rule); margin: var(--space-2) var(--space-1); background: var(--border); }
/* The company marks the row you are on with a star rather than a tick. */
.ls-menu__item[aria-checked="true"]::before,
.ls-menu__item[aria-selected="true"]::before { content: "\2605\a0"; }
/* Type comes from the technical-label recipe at the top of this file. */
.ls-menu__heading { padding: var(--space-2) var(--space-3) var(--space-1); }

/* ═══ Dialog ══════════════════════════════════════════════════ */
.ls-dialog {
  width: min(520px, calc(100vw - var(--space-8)));
  max-height: calc(100dvh - var(--space-16));
  /* Colonne flex, et c'est un correctif, pas une préférence. En flux de
     blocs, un contenu plus haut que max-height laissait le corps prendre
     sa pleine hauteur : l'en-tête + le corps + le pied dépassaient la
     boîte, qu'`overflow: hidden` se contentait de rogner. À l'ouverture,
     le focus part au bouton principal du pied — et le navigateur fait
     défiler même une boîte `overflow: hidden` pour l'atteindre : le
     dialogue s'ouvrait donc le titre et le bouton fermer HORS CHAMP, le
     haut du contenu tronqué, sans aucun défilement possible. Vu sur le
     choix de modèles de Photo à 390 px ; commun à tous les dialogues de
     la suite. La colonne borne le corps, qui devient la seule pièce qui
     défile — l'en-tête et le pied restent visibles par construction. */
  display: flex;
  flex-direction: column;
  padding: 0;
  /* A modal <dialog> is centred by the user agent's `margin: auto`, which
     the reset in base.css was zeroing along with everything else — so every
     dialog in the suite opened pinned to the top-left corner. Restored
     here rather than by weakening the reset, because only <dialog> relies
     on auto margins to position itself. */
  margin: auto;
  color: var(--text);
  background: var(--glass-strong);
  backdrop-filter: var(--glass-filter-heavy);
  -webkit-backdrop-filter: var(--glass-filter-heavy);
  border: var(--rule-strong) solid var(--border-ink);
  box-shadow: var(--shadow-xl), var(--shadow-sheen);
  overflow: hidden;
}
/* The page behind a dialog does not merely darken, it goes out of focus —
   which is what puts the dialog in front of it rather than on top of it,
   and what stops the field's halos reading through the sheet as noise. */
.ls-dialog::backdrop {
  background: var(--scrim);
  backdrop-filter: blur(6px) saturate(120%);
  -webkit-backdrop-filter: blur(6px) saturate(120%);
}
.ls-dialog[open] { animation: ls-dialog-in var(--duration-normal) var(--ease-spring); }
.ls-dialog[open]::backdrop { animation: ls-fade-in var(--duration-normal) var(--ease-out); }
@keyframes ls-dialog-in {
  from { opacity: 0; transform: scale(0.94) translateY(12px); }
  to   { opacity: 1; transform: none; }
}
@keyframes ls-fade-in { from { opacity: 0; } to { opacity: 1; } }

.ls-dialog[data-size="lg"] { width: min(760px, calc(100vw - var(--space-8))); }
.ls-dialog[data-size="sm"] { width: min(400px, calc(100vw - var(--space-8))); }

.ls-dialog__header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-5) var(--space-3);
  flex: none;
}
/* Anton is condensed, so it reads a size smaller than the grotesque it
   replaces; 20px here matches the optical weight the old 18px semibold
   had. Type comes from the display recipe at the top of this file. */
.ls-dialog__title { flex: 1; font-size: 1.25rem; padding-top: 1px; }
.ls-dialog__subtitle { margin-top: var(--space-1); font-size: var(--text-sm); color: var(--text-secondary); }
.ls-dialog__body {
  padding: var(--space-2) var(--space-5) var(--space-5);
  overflow-y: auto;
  /* min-height: 0, sans quoi un enfant flex refuse de descendre sous son
     contenu et le corps ne défile jamais — le défaut exact qu'on répare. */
  flex: 1 1 auto;
  min-height: 0;
  font-size: var(--text-md);
  color: var(--text-secondary);
  line-height: var(--leading-body);
}
.ls-dialog__footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  background: var(--surface-sunken);
  border-top: 1px solid var(--border-faint);
  flex: none;
}
.ls-dialog__footer[data-align="between"] { justify-content: space-between; }

/* ═══ Toast ═══════════════════════════════════════════════════ */
.ls-toast-region {
  position: fixed;
  inset-block-end: var(--space-5);
  inset-inline: 0;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  pointer-events: none;
}
.ls-toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: min(460px, calc(100vw - var(--space-8)));
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--glass-strong);
  backdrop-filter: var(--glass-filter-heavy);
  -webkit-backdrop-filter: var(--glass-filter-heavy);
  border: var(--rule-strong) solid var(--border-ink);
  box-shadow: var(--shadow-lg), var(--shadow-sheen);
  pointer-events: auto;
  animation: ls-toast-in var(--duration-normal) var(--ease-spring);
}
.ls-toast[data-leaving="true"] { animation: ls-toast-out var(--duration-fast) var(--ease-out) forwards; }
@keyframes ls-toast-in {
  from { opacity: 0; transform: translateY(16px) scale(0.94); }
  to   { opacity: 1; transform: none; }
}
@keyframes ls-toast-out {
  to { opacity: 0; transform: translateY(8px) scale(0.97); }
}
.ls-toast__icon { flex: none; }
.ls-toast[data-tone="success"] .ls-toast__icon { color: var(--ls-success); }
.ls-toast[data-tone="warning"] .ls-toast__icon { color: var(--ls-warning); }
.ls-toast[data-tone="danger"]  .ls-toast__icon { color: var(--ls-danger); }
.ls-toast[data-tone="info"]    .ls-toast__icon { color: var(--accent); }
.ls-toast__action {
  flex: none;
  padding: 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--accent);
  border-radius: var(--radius-sm);
}
.ls-toast__action:hover { text-decoration: underline; }

/* ═══ Tooltip ═════════════════════════════════════════════════ */
.ls-tooltip {
  position: fixed;
  z-index: var(--z-tooltip);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-inverse);
  background: var(--text);
  box-shadow: var(--shadow-sm);
  pointer-events: none;
  white-space: nowrap;
  animation: ls-fade-in var(--duration-instant) var(--ease-out);
}
.ls-tooltip kbd {
  padding: 0 0.3em;
  color: inherit;
  background: rgb(255 255 255 / 0.16);
  border: 0;
  opacity: 0.8;
}

/* ═══ Contextual help ═════════════════════════════════════════
   The bubble raised by packages/ui/help.js. Two lines: the control's name,
   then a sentence saying what it does.

   `white-space: normal` and a capped width are the whole point — the plain
   tooltip above is a no-wrap chip, and a sentence in it would be one long
   line. It is `position: fixed`, so that line would run past the edge of
   the screen and give the document a horizontal scrollbar nobody asked
   for. The `100vw` term is what holds at 320 px, where 34ch of German is
   wider than the phone. */
.ls-help {
  display: block;
  max-width: min(34ch, calc(100vw - 2 * var(--space-4)));
  padding: var(--space-2) var(--space-3);
  white-space: normal;
  line-height: var(--leading-snug);
}
.ls-help__name { display: block; font-weight: var(--weight-semi); }
.ls-help__body { display: block; margin-top: 2px; opacity: 0.85; }
/* `display: block` above would otherwise beat the native `hidden`. */
.ls-help[hidden] { display: none; }

/* ═══ Chip / Tag ══════════════════════════════════════════════ */
.ls-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: 22px;
  padding: 0 var(--space-2);
  font-size: var(--text-2xs);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  background: var(--surface-inset);
  border: var(--rule) solid var(--border);
  white-space: nowrap;
}
.ls-chip[data-tone="accent"] { color: var(--accent); background: var(--accent-wash); }
/* La chip de LECTURE reste à 22 px : c'est une information, pas une cible.
   Une chip qui COMMANDE — « + ajouter une étiquette » — est un bouton, et
   suit la règle de tous les boutons de la suite : la hauteur du pointeur,
   34 px à la souris, 44 px au doigt, par le jeton que tout le monde suit
   déjà. Le pointillé dit « emplacement à remplir », pas « état posé ». */
.ls-chip--action {
  min-height: var(--tap-target);
  padding: 0 var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  background: transparent;
  border-style: dashed;
  cursor: pointer;
  transition: var(--transition-control);
}
.ls-chip--action:hover { color: var(--accent); background: var(--accent-wash); border-color: var(--accent); }
.ls-chip__remove { display: inline-flex; margin-inline-end: -2px; color: var(--text-faint); border-radius: var(--radius-full); }
.ls-chip__remove:hover { color: var(--ls-danger); }

/* ═══ Divider & toolbar group ═════════════════════════════════ */
.ls-divider { flex: none; width: 1px; height: 20px; margin: 0 var(--space-1); background: var(--border); }
.ls-divider[data-orientation="horizontal"] { width: 100%; height: 1px; margin: var(--space-2) 0; }
.ls-group { display: flex; align-items: center; gap: 2px; }

/* ═══ Empty state ═════════════════════════════════════════════
   The one place in a working application with room to say who made it.
   The art is the company mark's own structure rebuilt from the shared
   forms — a glass node inside a broken orbit, with the star sitting in
   the gap — so an empty Calendar, an empty Vector canvas and an empty
   Video timeline are recognisably the same product. The title takes the
   poster voice and the explanation takes the serif: one is a headline,
   the other is meant to be read. */
.ls-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-12) var(--space-6);
  text-align: center;
  color: var(--text-muted);
}
.ls-empty__art {
  --art: 76px;
  position: relative;
  display: grid;
  place-items: center;
  width: var(--art);
  height: var(--art);
  margin-bottom: var(--space-2);
  color: var(--accent);
  background: var(--accent-wash);
  /* Round, not a squircle: it is the body on the orbit. */
  border-radius: var(--radius-full);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent),
    var(--shadow-sheen);
}
/* The orbit, drawn outside the node it circles. */
.ls-empty__art::before {
  content: "";
  position: absolute;
  inset: -19%;
  background: currentColor;
  opacity: 0.42;
  -webkit-mask: var(--shape-halo) center / contain no-repeat;
  mask: var(--shape-halo) center / contain no-repeat;
  pointer-events: none;
}
.ls-empty__art .ls-icon { width: 30px; height: 30px; stroke-width: 1.5; }
.ls-empty__title {
  font-size: var(--display-sm);
  color: var(--text);
}
/* Prose, so it gets prose contrast: --text-muted is 4.3:1 on the glass and
   this is a paragraph somebody is expected to read, not a caption. */
.ls-empty__text { max-width: 46ch; font-size: var(--text-md); color: var(--text-secondary); }
/* Used on its own inside a rail — a hint rather than a whole empty state,
   so it stays quiet and does not get a headline's air. */
.ls-empty__text:only-child { font-size: var(--text-sm); line-height: var(--leading-body); }

/* ═══ Colour ══════════════════════════════════════════════════
   One grid, one swatch, one selected state, in every application.

   The swatches used to sit in an eight-column grid with no room around
   them, so a row of circles ran together into a single striped bar and it
   was hard to tell where one colour ended and the next began. They are now
   laid out on a fixed 26px track with a gap wider than the ring they grow,
   which is what makes them read as individual objects.

   The selected ring is drawn *outside* the swatch, in the swatch's own
   colour, separated by a band of the surface. Painting the ring on top of
   the swatch — which is what the old box-shadow did — changed the very
   colour you were trying to judge. */
.ls-swatches {
  display: grid;
  grid-template-columns: repeat(6, 26px);
  gap: 10px;
  padding: var(--space-2);
}
.ls-swatches__rule { grid-column: 1 / -1; height: 1px; background: var(--border); margin: 2px 0; }

.ls-swatch {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--swatch, transparent);
  cursor: pointer;
  transition: var(--transition-control);
  /* A hairline so white and near-white stay visible on a light surface,
     and so every swatch has the same silhouette whatever it contains. */
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.16);
}
[data-theme="dark"] .ls-swatch { box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.18); }

.ls-swatch:hover { transform: scale(1.14); z-index: 1; }
.ls-swatch:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.ls-swatch[aria-checked="true"],
.ls-swatch[aria-pressed="true"] {
  box-shadow:
    inset 0 0 0 1px rgb(0 0 0 / 0.16),
    0 0 0 2px var(--surface),
    0 0 0 4px var(--swatch, var(--accent));
}
[data-theme="dark"] .ls-swatch[aria-checked="true"],
[data-theme="dark"] .ls-swatch[aria-pressed="true"] {
  box-shadow:
    inset 0 0 0 1px rgb(255 255 255 / 0.18),
    0 0 0 2px var(--surface),
    0 0 0 4px var(--swatch, var(--accent));
}

/* "No colour" — the diagonal is the universal sign for it, and it means
   the swatch does not have to be mistaken for white. */
.ls-swatch--none {
  background:
    linear-gradient(to top right, transparent calc(50% - 1px), var(--danger) calc(50% - 1px),
      var(--danger) calc(50% + 1px), transparent calc(50% + 1px)),
    var(--surface-inset);
}

/* The system colour picker, wearing the same silhouette as the roster. */
.ls-swatch--custom { overflow: hidden; -webkit-appearance: none; appearance: none; }
.ls-swatch--custom::-webkit-color-swatch-wrapper { padding: 0; }
.ls-swatch--custom::-webkit-color-swatch { border: 0; border-radius: var(--radius-full); }
.ls-swatch--custom::-moz-color-swatch { border: 0; border-radius: var(--radius-full); }

/* Toolbar control: the current colour, and one click to the grid. */
.ls-colour-btn__chip {
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--swatch, var(--text));
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.2);
}
[data-theme="dark"] .ls-colour-btn__chip { box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.22); }

/* A menu that draws a panel instead of rows. */
.ls-menu__panel { padding: 0; }

/* ═══ Progress ════════════════════════════════════════════════ */
.ls-progress { width: 100%; height: 4px; overflow: hidden; background: var(--surface-inset); border: var(--rule) solid var(--border); }
.ls-progress__bar { height: 100%; background: var(--accent-gradient); border-radius: inherit; transition: width var(--duration-normal) var(--ease-out); }

/* ═══ Spinner ═════════════════════════════════════════════════
   Waiting is an orbit, not a chasing arc: the same broken ring with the
   same star in the gap that the empty states and the launcher carry,
   turning once every one and a bit seconds. The suite has exactly one
   idea about circles and this is it. */
.ls-spinner {
  width: 20px;
  height: 20px;
  background: var(--accent);
  -webkit-mask: var(--shape-halo) center / contain no-repeat;
  mask: var(--shape-halo) center / contain no-repeat;
  animation: ls-spin 1.15s linear infinite;
}
.ls-spinner[data-size="lg"] { width: 40px; height: 40px; }
@keyframes ls-spin { to { transform: rotate(360deg); } }
/* Reduced motion keeps the mark, drops the rotation — a still orbit still
   reads as "working", a strobing one does not. */
@media (prefers-reduced-motion: reduce) {
  .ls-spinner { animation-duration: 4s; }
}

/* ═══ Astralaika ownership badge ══════════════════════════════
   Present in every application. Subtle by design: it signs the
   product without competing with it. */
.ls-astra {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-muted);
  transition: var(--transition-control);
}
.ls-astra:hover { color: var(--text); background: var(--hover); text-decoration: none; }
.ls-astra__mark {
  width: 15px;
  height: 15px;
  opacity: 0.72;
  transition: opacity var(--duration-fast) var(--ease-out), transform var(--duration-normal) var(--ease-spring);
}
.ls-astra:hover .ls-astra__mark { opacity: 1; transform: scale(1.1) rotate(-6deg); }
.ls-astra__name { font-weight: var(--weight-medium); }

/* ═══ Scroll shadow helper ════════════════════════════════════
   Fades content at a scroll boundary instead of drawing a hard rule. */
.ls-scroll-fade {
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 12px), transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 12px), transparent);
}

/* ═══ Reduced transparency — an accessibility preference some
   users set at OS level; honour it by trading glass for solid.

   The same list the @supports fallback at the top of this file carries,
   for the same reason: a surface left off it is a surface that stays
   translucent for the one user who asked it not to. The lit edge stays —
   it is the only thing left telling a raised pane from the page. ═══════ */
@media (prefers-reduced-transparency: reduce) {
  .ls-glass, .ls-panel, .ls-menu, .ls-dialog, .ls-toast,
  .ls-topbar, .ls-toolbar, .ls-rail, .ls-statusbar,
  .bar, .bar-control, .burger, .menu-overlay, .directive, .stamp,
  .languages-panel, .ls-btn[data-variant="secondary"] {
    background: rgb(var(--pane-rgb));
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .ls-toolbar, .ls-rail, .ls-statusbar, .menu-overlay {
    background: var(--surface-sunken);
  }
  .ls-dialog::backdrop { backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* ═══ Language button ═════════════════════════════════════════
   A globe alone says "there is a language menu"; it does not say which
   language you are in. The short code sits beside it, in the same
   "EN"/"FR" form the company website uses. */
.ls-lang-btn {
  width: auto;
  gap: var(--space-1);
  padding-inline: var(--space-2);
}
.ls-lang-btn__code {
  font-size: var(--text-2xs);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-wide);
  font-variant-numeric: tabular-nums;
}

/* ═══ Select — the visible control ════════════════════════════
   A native <select> draws its list with the operating system, which
   ignores the theme and renders pale-on-pale here. packages/ui/select.js
   hides the native element and puts this button in front of it, opening
   the same .ls-menu the language picker uses — so every dropdown in the
   suite is one object with one appearance. */
.ls-select--button {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  text-align: start;
  /* The chevron is a real element here, not a background image. */
  background-image: none;
  padding-inline-end: var(--space-2);
  cursor: pointer;
}
.ls-select__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ls-select__chevron { display: flex; flex: none; color: var(--text-faint); }
.ls-select--button:hover .ls-select__chevron { color: var(--text-secondary); }
.ls-select--button[aria-expanded="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

/* Inside a toolbar the control stays compact, matching the old native one. */
.ls-toolbar .ls-select--button {
  flex: none;
  width: auto;
  min-width: 8.5rem;
  min-height: 30px;
  padding: 0 var(--space-2);
  font-size: var(--text-xs);
  background-color: transparent;
  border-color: transparent;
  border-radius: var(--radius-sm);
}
.ls-toolbar .ls-select--button:hover { background-color: var(--hover); }

/* ═══ Document library ════════════════════════════════════════
   The same panel, with the same appearance, in every application. It is a
   drawer rather than a file manager: one search box, one list, and a trash
   behind the second tab. Nothing here is application specific on purpose —
   Write and Video must be indistinguishable at this level. */
.ls-library {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-height: 18rem;
}
.ls-library__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}
.ls-library__bar .ls-search { flex: 1 1 12rem; min-width: 0; }
.ls-library__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-height: min(52vh, 26rem);
  overflow-y: auto;
  margin-inline: calc(var(--space-2) * -1);
  padding-inline: var(--space-2);
}
.ls-library__row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding-inline-end: var(--space-1);
  border-radius: var(--radius-md);
  transition: var(--transition-control);
}
.ls-library__row:hover { background: var(--hover); }
.ls-library__row[data-active="true"] { background: var(--selected); }
.ls-library__open {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3);
  text-align: start;
  border-radius: var(--radius-md);
  cursor: pointer;
}
.ls-library__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}
.ls-library__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  color: var(--text);
}
.ls-library__row[data-active="true"] .ls-library__name { color: var(--accent); }
/* A timestamp is data, so it is set in the instrument face. */
.ls-library__time {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.ls-library__preview {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.ls-library__empty {
  padding: var(--space-10) var(--space-4);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The button that opens it. Beside the document title in every
   application, never inside an overflow menu — a list of your previous
   work is not a setting. The label folds away on a narrow screen; the
   folder mark alone still reads as "your files". */
.ls-library-btn { flex: none; gap: var(--space-2); }
.ls-library-btn__label { white-space: nowrap; }
@media (max-width: 720px) {
  .ls-library-btn__label { display: none; }
}

/* ═══ First-run introduction ══════════════════════════════════
   Three slides, centred, with the product's own mark at the top so the
   panel reads as a welcome rather than as a dialog that went wrong. */
.ls-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  text-align: center;
}
.ls-intro__mark {
  width: 52px;
  height: 52px;
  margin-bottom: var(--space-1);
}
.ls-intro__mark svg, .ls-intro__mark img { width: 100%; height: 100%; }
.ls-intro__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-semi);
  color: var(--text);
}
.ls-intro__body {
  max-width: 38ch;
  font-size: var(--text-md);
  line-height: var(--leading-body);
  color: var(--text-secondary);
}
.ls-intro__dots { display: flex; gap: var(--space-2); margin-top: var(--space-1); }
.ls-intro__dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border-radius: var(--radius-full);
  background: var(--border-strong);
  cursor: pointer;
  transition: var(--transition-control);
}
.ls-intro__dot[aria-current="true"] { width: 18px; background: var(--accent); }
.ls-intro__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: pointer;
}

/* ═══ Passphrase forms ════════════════════════════════════════
   The Vault's own dialogs and the "save this to a vault" dialogs every
   other application now shows are the same dialogs, so their geometry
   lives here rather than in one application's stylesheet. */

.ls-secretform { display: grid; gap: var(--space-4); }
.ls-secretform__secrets { display: grid; gap: var(--space-3); }
/* `display: grid` outranks the user-agent rule for [hidden], and a
   passphrase field that is still on screen before the warning has been
   acknowledged defeats the entire point of the warning. */
.ls-secretform__secrets[hidden] { display: none; }
.ls-secret { font-family: var(--font-mono); letter-spacing: 0.02em; }

/* The one interruption in the product. It is loud on purpose: the
   passphrase fields do not exist until it has been acknowledged. */
.ls-warnbox {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2) var(--space-3);
  padding: var(--space-4);
  background: color-mix(in srgb, var(--ls-warning) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--ls-warning) 42%, transparent);
  border-radius: var(--radius-lg);
}
.ls-warnbox__icon { display: flex; color: var(--ls-warning); }
.ls-warnbox__text {
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  color: var(--text);
}
.ls-warnbox__accept {
  grid-column: 2;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text);
  cursor: pointer;
}
.ls-warnbox__box { width: 16px; height: 16px; accent-color: var(--accent); }
.ls-warnbox[data-accepted="true"] {
  background: transparent;
  border-color: var(--border);
}

.ls-strength { display: flex; align-items: center; gap: var(--space-3); }
.ls-strength__label {
  flex: none;
  font-size: var(--text-2xs);
  font-weight: var(--weight-semi);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.ls-strength .ls-progress { flex: 1; height: 6px; }
.ls-strength[data-level="weak"] .ls-progress__bar { background: var(--ls-danger); }
.ls-strength[data-level="fair"] .ls-progress__bar { background: var(--ls-warning); }
.ls-strength[data-level="strong"] .ls-progress__bar { background: var(--ls-success); }

/* ═══ The vault picker ════════════════════════════════════════
   Choosing which vault, and choosing which document inside it. Both are
   lists of one-line rows with a name and a quiet second line. */

.ls-vaultlist { display: grid; gap: var(--space-1); max-height: 52vh; overflow-y: auto; }
.ls-vaultlist__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  text-align: start;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: var(--transition-control);
}
.ls-vaultlist__row:hover { background: var(--surface-sunken); border-color: var(--border-faint); }
.ls-vaultlist__row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ls-vaultlist__icon { display: flex; flex: none; color: var(--text-faint); }
.ls-vaultlist__text { display: grid; gap: 1px; min-width: 0; flex: 1; }
.ls-vaultlist__name {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ls-vaultlist__meta { font-size: var(--text-2xs); color: var(--text-faint); }


/* ═══ Save to Vault ═══════════════════════════════════════════
   The one control in every application that answers "will this still be
   here tomorrow?", so it is the only primary button in the top bar and it
   never hides behind an overflow. The label names both the familiar verb
   and what actually happens — "Save (add to Vault)" — because the
   parenthetical is how somebody learns what a Vault is without having to
   read about it first. */
.ls-save-btn { flex: none; gap: var(--space-2); white-space: nowrap; }
/* Below the width where a top bar starts fighting itself, the verb goes
   and the shield stays: the button keeps its place and its meaning. */
@media (max-width: 720px) {
  .ls-save-btn__label { display: none; }
}


/* ═══ STORAGE UI — owned by the storage pass ═══ */

/* ── The start screen ─────────────────────────────────────────
   Shown in the main surface when an application has nothing to open, and
   only then. It covers the workspace and stops short of the two bars that
   frame it: the top bar keeps the document list and the Save button, the
   status bar keeps the readout saying where work goes. Somebody arriving
   for the first time should be able to see all three at once — where they
   are about to start, where it will be kept, and how to get back to it. */
/* Nothing open yet.

   This was a translucent pane inset below the top bar, so the editor,
   the search field and the tag list of an empty application showed
   through it, and the toolbar beneath stayed live — pressing the menu
   opened a document menu for a document that did not exist.

   It is a *state*, not an overlay. The workspace and toolbar are hidden
   outright while it is up, and the sheet is the page's own field rather
   than glass over something. */
[data-start="true"] .ls-toolbar,
[data-start="true"] .ls-workspace { display: none; }

.ls-start {
  position: fixed;
  inset: var(--topbar-h) 0 var(--statusbar-h) 0;
  z-index: var(--z-overlay);
  display: grid;
  place-items: center;
  padding: var(--space-6);
  overflow-y: auto;
  background: var(--field);
  background-attachment: fixed;
  animation: ls-start-in var(--duration-normal) var(--ease-out) both;
}
@keyframes ls-start-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ls-start { animation: none; }
}
.ls-start__panel {
  width: min(46rem, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  text-align: center;
}
.ls-start__mark { width: 56px; height: 56px; }
.ls-start__mark svg { width: 100%; height: 100%; }
.ls-start__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--display-xs);
  font-weight: var(--font-display-weight);
  letter-spacing: var(--tracking-display);
  color: var(--text);
}
.ls-start__subtitle {
  margin: 0;
  max-width: 38rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* One row on a desk, one column on a phone — the choices are the screen,
   so they get the width rather than the prose above them. */
.ls-start__choices {
  display: grid;
  gap: var(--space-3);
  /* A capped track rather than a free 1fr: an application with one choice
     to offer should not draw one card three columns wide, and the row
     stays centred however many there are. */
  grid-template-columns: repeat(auto-fit, minmax(13rem, 15rem));
  justify-content: center;
  width: 100%;
  margin-top: var(--space-3);
}
.ls-start__choice {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-5);
  text-align: start;
  font: inherit;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: var(--transition-control);
}
.ls-start__choice:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}
.ls-start__choice:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .ls-start__choice:hover { transform: none; }
}
.ls-start__choiceIcon { display: flex; color: var(--accent); }
.ls-start__choiceLabel {
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
}
.ls-start__choiceHint {
  font-size: var(--text-xs);
  line-height: var(--leading-body);
  color: var(--text-muted);
}

/* The case for the Vault, made once, to somebody who is deciding where
   their work is about to go. */
.ls-start__why {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  width: 100%;
  margin-top: var(--space-4);
  padding: var(--space-4);
  text-align: start;
  background: var(--surface-sunken);
  border: 1px solid var(--border-faint);
  border-radius: var(--radius-md);
}
.ls-start__whyIcon { display: flex; flex: none; color: var(--ls-success); }
.ls-start__whyTitle {
  margin: 0 0 2px;
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--text);
}
.ls-start__whyText {
  margin: 0;
  font-size: var(--text-xs);
  line-height: var(--leading-body);
  color: var(--text-muted);
}

/* ── Where a document is kept ─────────────────────────────────
   The same two colours as the status-bar indicator, on a chip small
   enough to sit in a list row: green is a named vault, orange is the
   browser cache and nothing else. A chip with somewhere to go is a
   button; one that is already there is a label. */
.ls-store {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  max-width: 12rem;
  padding: 2px var(--space-2);
  font: inherit;
  font-size: var(--text-2xs);
  color: var(--text-muted);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  transition: var(--transition-control);
}
button.ls-store { cursor: pointer; }
button.ls-store:hover { border-color: currentColor; }
button.ls-store:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button.ls-store:disabled { opacity: 0.6; cursor: progress; }
.ls-store__dot {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: var(--radius-full);
  background: var(--ls-warning);
}
.ls-store__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ls-store[data-state="vault"],
.ls-store[data-state="saving"] {
  color: color-mix(in srgb, var(--ls-success) 72%, var(--text));
  background: color-mix(in srgb, var(--ls-success) 10%, transparent);
}
.ls-store[data-state="vault"] .ls-store__dot,
.ls-store[data-state="saving"] .ls-store__dot { background: var(--ls-success); }
.ls-store[data-state="local"],
.ls-store[data-state="locked"] {
  color: color-mix(in srgb, var(--ls-warning) 68%, var(--text));
  background: color-mix(in srgb, var(--ls-warning) 14%, transparent);
}
.ls-store[data-state="failed"] {
  color: color-mix(in srgb, var(--ls-danger) 72%, var(--text));
  background: color-mix(in srgb, var(--ls-danger) 12%, transparent);
}
.ls-store[data-state="failed"] .ls-store__dot { background: var(--ls-danger); }

/* A write in progress pulses rather than spins: it is usually over in one
   frame, and a spinner that appears and vanishes reads as a glitch. */
.ls-store[data-state="saving"] .ls-store__dot,
.ls-where[data-state="saving"] .ls-where__dot {
  animation: ls-store-pulse var(--duration-slower) var(--ease-inout) infinite;
}
@keyframes ls-store-pulse {
  50% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
  .ls-store[data-state="saving"] .ls-store__dot,
  .ls-where[data-state="saving"] .ls-where__dot { animation: none; }
}

/* The status-bar readout gained two more truths to tell: a vault that is
   locked, and a write that did not land. Both are the browser-only
   colour, because that is what they both mean for the work on screen. */
.ls-where[data-state="saving"] {
  color: color-mix(in srgb, var(--ls-success) 72%, var(--text));
  background: color-mix(in srgb, var(--ls-success) 10%, transparent);
}
.ls-where[data-state="saving"] .ls-where__dot { background: var(--ls-success); }
.ls-where[data-state="locked"] {
  color: color-mix(in srgb, var(--ls-warning) 68%, var(--text));
  background: color-mix(in srgb, var(--ls-warning) 14%, transparent);
}
.ls-where[data-state="failed"] {
  color: color-mix(in srgb, var(--ls-danger) 72%, var(--text));
  background: color-mix(in srgb, var(--ls-danger) 12%, transparent);
}
.ls-where[data-state="failed"] .ls-where__dot { background: var(--ls-danger); }

/* Room for the chip: the name and preview give up the width, not the
   answer to "is this document actually kept anywhere". */
.ls-library__row .ls-store { margin-inline-start: var(--space-2); }
@media (max-width: 560px) {
  .ls-library__row .ls-store__text { display: none; }
  .ls-library__row .ls-store { padding-inline: var(--space-1); }
}

/* ── The storage panel ────────────────────────────────────────
   What the readout opens: where this document is, in a sentence, and the
   one action available from here. */
.ls-storage { display: grid; gap: var(--space-3); }
.ls-storage__state {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  color: var(--text);
}
.ls-storage__state[data-state="vault"] .ls-store__dot,
.ls-storage__state[data-state="saving"] .ls-store__dot { background: var(--ls-success); }
.ls-storage__state[data-state="failed"] .ls-store__dot { background: var(--ls-danger); }
.ls-storage__detail {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  color: var(--text-muted);
}
.ls-storage__why {
  padding: var(--space-4);
  background: var(--surface-sunken);
  border: 1px solid var(--border-faint);
  border-radius: var(--radius-md);
}
.ls-storage__whyTitle {
  margin: 0 0 var(--space-1);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--text);
}
.ls-storage__whyText {
  margin: 0;
  font-size: var(--text-xs);
  line-height: var(--leading-body);
  color: var(--text-muted);
}

/* ── The document list button ─────────────────────────────────
   It matches the Save button beside it now: same height, same weight of
   label, same corner of the bar. It was a small ghost control next to the
   title and it was not being found, which is a fair verdict on the way
   back to everything you have ever made here. */
.ls-library-btn { flex: none; gap: var(--space-2); }

/* Several savable kinds means several lines in the panel: the rule is that
   a thing an application offers to save has to be able to say, somewhere a
   person can look, whether it actually is saved. This is that place. */
.ls-storage__row { display: grid; gap: var(--space-1); justify-items: start; }
.ls-storage__row + .ls-storage__row {
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-faint);
}
/* The kind chooser puts each kind's state beside its name, so you can see
   which one is not in a vault before you pick it. */
.ls-vaultlist__row .ls-store { margin-inline-start: auto; padding: 0; }

/* Two documents may share a name. The list does not argue with that — it
   stops leaning on "2 minutes ago", which cannot tell them apart, and
   shows the date and time, which can. */
.ls-library__time[data-exact="true"] { color: var(--text-muted); }
