/*
 * Flowi editor chrome.
 *
 * Design principles applied here, in priority order:
 *
 *  1. The canvas is the product. Chrome is quiet, low-contrast and thin so
 *     that the user's work is the only saturated thing on screen.
 *  2. Progressive disclosure. The inspector shows what applies to the current
 *     selection; there is no wall of permanently-greyed controls.
 *  3. Keyboard first. Every interactive element has a visible focus ring, and
 *     focus is never removed without a replacement.
 *  4. Respect the system, and let it be overruled. Reduced motion and forced
 *     colours are honoured rather than overridden; the theme follows
 *     `prefers-color-scheme` until somebody says otherwise, and `theme.ts` is
 *     where "otherwise" is stored.
 */

/*
 * The palette, in four blocks, and the order of them is the whole mechanism.
 *
 *   :root                                  the light palette, and everything
 *                                          that is not about theme at all
 *   @media (prefers-color-scheme: dark)    what the machine asks for
 *   :root[data-theme='light']              what the person asked for
 *   :root[data-theme='dark']
 *
 * The two `[data-theme]` blocks come **after** the media query, and each lists
 * every theme-dependent token rather than a diff against the block above. Not
 * belt and braces: on a dark machine the media query is still in force, so a
 * light block that only overrode "the tokens that look wrong" would leave every
 * token it forgot at its dark value — a light theme that is partly dark, on
 * exactly the machines whose owners are least likely to notice. Specificity
 * happens to favour `:root[data-theme]` over `:root`, and relying on that would
 * still not help for a token the light block never mentions.
 *
 * `theme.ts` writes the attribute, and writes *nothing* for `auto` — with no
 * `data-theme` the media query answers, which is the browser following the
 * system for us and going on following it after load.
 *
 * A token that is genuinely not about theme belongs in `:root` and in neither
 * block below; `theme.test.ts` reads this file and holds the split, so the list
 * is derived rather than maintained.
 *
 * ## Where these colours come from (update 125)
 *
 * Every value below is a **role** out of UI3, Figma's own interface kit, read
 * through its MCP by the frames that resolve it — light values from the
 * `Light` frames of the Color pages, dark values from the `Dark` ones beside
 * them. `packages/design-system/src/ui3.ts` carries the whole set as tokens
 * and records the file key and the frame ids, so the next person can re-read
 * them rather than trust a comment. Each token below names the role it came
 * from where the mapping is not obvious.
 *
 * Three of them are **not** UI3's, and each says so at the line: the component
 * purple (UI3 has no such concept), the eight identity colours (nor those),
 * and the type face — UI3's display face is Whyte, which is Figma's licensed
 * font and not ours to ship, and its body face is Inter, which is open and
 * which this deployment still has no way to serve: the content policy is
 * `font-src 'self'` and there is no font in the tree. So the stack stays the
 * system one, which is a decision rather than an omission.
 */

:root {
  /* Surfaces. `bg.default` is the panel; `bg.secondary` is the canvas under
     it, which is what makes the artwork sit *on* something rather than bleed
     into the chrome. */
  --bg: #ffffff;
  --bg-sunken: #f1f3f8;
  --panel: #ffffff;
  --border: #e2e6ee;
  /* `ramp.grey.300`. UI3 names one border colour per context and no "strong"
     one, so the emphasis step comes from the ramp the roles are built on
     rather than from a colour picked by eye. */
  --border-strong: #cad0db;
  /* Ink at an opacity, which is the whole of UI3's grey system: `text.default`
     is #0f1216 at 90%, not a flattened grey. That is why a secondary label
     stays right when it lands on the canvas, on a card, or on a selected row —
     a flattened grey is correct on exactly one surface. */
  --text: rgba(15, 18, 22, 0.9);
  --text-muted: rgba(15, 18, 22, 0.5);
  --text-subtle: rgba(15, 18, 22, 0.3);
  --accent: #847bff;
  --accent-soft: #f1f2ff;
  /* Accent text *on* `--accent-soft`, which `--accent` itself does not reach.
     Bug 466: `#847bff` on the soft chip measures 2.94:1 in dark and 3.36:1 in
     light, and it is the colour of every "you are here" mark in the product —
     the selected rail scope, the active mode, the chosen code target. The pair
     is fixed with a deeper (light) or lighter (dark) step of the same ramp
     rather than by moving `--accent`, because `--accent` is also a *background*
     under `--on-accent`, and lightening it would trade one shortfall for
     another. Found by `tools/probe-visual.mjs`, which is the first thing in
     this repository that has ever measured a contrast ratio. */
  --on-accent-soft: #5a4fd6;
  /* The accent as a **fill under white**, which `--accent` itself does not
     carry: `#ffffff` on `#847bff` measures 3.36:1, and that is the label on
     every primary button in the product. Same reasoning as `--on-accent-soft`
     above and the same ramp step — `--accent` stays UI3's `color.bg.brand`,
     because `ui3.test.ts` holds that decision and it is the right one; what is
     ours is the pairing, so the new token is ours too. */
  --accent-fill: #5a4fd6;
  --on-accent: #ffffff;
  --on-danger: #ffffff;
  /* Components. UI3 has no role for "this is a component" — it is our concept,
     not Figma's UI kit's — so it is the purple ramp's own deeper step: related
     to the brand, and never the same colour as a selection. */
  --component: #6b5ae8;
  --component-soft: #e2e4ff;
  --hover: rgba(15, 18, 22, 0.1);
  /* Inspector fields: a *fill* rather than an outline.
     Figma's right panel draws its inputs as filled boxes a step away from the
     panel behind them, with the border appearing only under the pointer. A
     panel of thirty outlined rectangles reads as thirty separate objects; the
     same panel filled reads as values sitting in a surface, which is what they
     are. `--field-border` is transparent on purpose rather than absent: the box
     keeps its 1px, so nothing shifts by a pixel when the border appears. */
  --field: #f1f3f8;
  --field-hover: #e7eaf2;
  --field-border: transparent;
  --danger: #ca3b3e;
  --danger-soft: #ffddda;
  --positive: #008c5f;
  --warning: #9b6c00;
  /* The soft companion, for a banner rather than a word. Beside `--danger-soft`
     and for the same reason: a warning painted as text on the panel colour is a
     sentence people read past. */
  --warning-soft: #fdf1d6;
  /* `shadow.light.elevation.400.menu`, all three of its layers. A panel's
     shadow in UI3 is a stack — a tight contact shadow, a wide ambient one and
     a hairline doing the work of a 1px border — and flattening it to one blur
     is what makes a floating panel look pasted on. */
  --shadow-panel:
    0 2px 5px rgba(0, 0, 0, 0.15),
    0 10px 16px rgba(0, 0, 0, 0.12),
    0 0 0.5px rgba(0, 0, 0, 0.12);

  /* The transparency checkerboard, under a swatch or a gradient stop. */
  --checker-a: #ffffff;
  --checker-b: #e2e6ee;
  /* `ramp.black.300`: the hairline round a swatch, so a pale colour still has
     an edge. */
  --swatch-ring: rgba(15, 18, 22, 0.2);

  /* The template thumbnails in the file browser, drawn in CSS. */
  --art-ink: #1d2027;
  --art-wash-a: #e2e4ff;
  --art-wash-b: #f1f2ff;
  --art-accent: #c3f4da;

  /* ---- not about theme -------------------------------------------------- */

  /* -------------------------------------------------------- the type scale

     UI3's, read from the kit's Typography page and held against it by
     `type-scale.test.ts` — the same arrangement as the colour roles, and for
     the same reason: a number transcribed by hand into a stylesheet is a
     number that stops being the kit's the first time either side moves.

     Not in the four palette blocks below, because type does not change with
     the theme. Each step is three properties, because a size without its
     tracking and leading is a third of a type style and the two thirds left
     behind are exactly where an interface stops looking like one thing.

     `heading.medium` is deliberately absent: in the kit its size resolves
     through `body/medium/fontSize` to 11 under a line height of 25, which is
     either a mis-wired alias or a stale specimen. `ui3.ts` records the doubt;
     a stylesheet that used it would be spending the doubt. */
  --type-body-small: 9px;
  --type-body-small-tracking: 0.5px;
  --type-body-small-line: 14px;
  --type-body-medium: 11px;
  --type-body-medium-tracking: 0.5px;
  --type-body-medium-line: 16px;
  --type-body-large: 13px;
  --type-body-large-tracking: -0.25px;
  --type-body-large-line: 22px;
  --type-heading-small: 13px;
  --type-heading-small-tracking: -0.25px;
  --type-heading-small-line: 22px;
  --type-heading-large: 24px;
  --type-heading-large-tracking: -1.7px;
  --type-heading-large-line: 32px;
  --type-heading-display: 48px;
  --type-heading-display-tracking: -3px;
  --type-heading-display-line: 56px;

  --toolbar-height: 48px;
  --status-height: 26px;
  /* The floating tool pill is dark in both themes: it reads as an instrument
     over the artwork rather than as another panel. So are the four tokens that
     describe it, and that is why they are up here and in neither block below. */
  --dock: #2e323b;
  --dock-text: #ffffff;
  --dock-muted: rgba(255, 255, 255, 0.7);
  --dock-hover: rgba(255, 255, 255, 0.1);
  /* The scrim under a modal. It darkens whatever is behind it, and "whatever is
     behind it" is the only thing it needs to know — a lighter scrim in the
     light theme would make the dialog float over a fog rather than over the
     application. */
  --overlay: rgba(15, 18, 22, 0.5);

  /* Who is who: eight avatar colours and the same eight for cursors. Identical
     in both themes, and that is the requirement rather than an accident — a
     person whose colour changed when a *colleague* switched theme would stop
     being recognisable, which is the one job these have. They were written out
     twice, as eight `.avatar[data-colour]` rules and eight `.auth-swatch` ones;
     two lists that have to agree and nothing that checked they did. */
  --identity-0: #6c5cff;
  --identity-1: #0f9d76;
  --identity-2: #e5484d;
  --identity-3: #d97706;
  --identity-4: #2f6bff;
  --identity-5: #c026d3;
  --identity-6: #0891b2;
  --identity-7: #4d5560;

  --sidebar-width: 248px;
  --radius: 5px;
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;

  /* How solid the icons are drawn. Here, in the first `:root` block, and not
     down beside the icon rules: the dark blocks below lower `--icon-ink`, and a
     second `:root` later in the file would have the same specificity and win by
     order — the icons would have been tuned for a light background in every
     theme, and the token would have looked like it did nothing (lesson 142).

     Material Symbols had a GRAD axis for this and Phosphor does not: a weight
     is a whole font file there, and this app ships the regular and fill cuts
     only. So the dark-theme correction is opacity rather than stroke weight —
     less precise, and the honest tool the set actually gives. */
  --icon-ink: 1;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #2e323b;
    /* `ramp.grey.900`, which is what UI3 calls `bg.menu` — its darkest named
       surface. The canvas has to sit *below* the panels in the dark theme too,
       and UI3's dark `bg.secondary` resolves to the same value as
       `bg.default`, so it cannot be that. */
    --bg-sunken: #1d2027;
    --panel: #2e323b;
    --border: #414753;
    --border-strong: #6e7687;
    --text: #ffffff;
    --text-muted: rgba(255, 255, 255, 0.7);
    --text-subtle: rgba(255, 255, 255, 0.4);
    --accent: #847bff;
    --accent-soft: #403396;
    --on-accent-soft: #b0a8ff;
  --accent-fill: #6b60e0;
    --accent-fill: #6b60e0;
    --on-accent: #ffffff;
    --on-danger: #ffffff;
    --component: #a8a8ff;
    --component-soft: #403396;
    --hover: rgba(255, 255, 255, 0.1);
    /* Lighter than the panel here, where the light theme's is darker: a fill
       has to step *away* from its surface, and which direction that is depends
       on the surface. Sinking a field on a dark panel makes it a hole. */
    --field: #3a404b;
    --field-hover: #434a57;
    --field-border: transparent;
    --danger: #ff8d87;
    --danger-soft: #811b1f;
    --positive: #5fcb9a;
    --warning: #f4c781;
    --warning-soft: #4a3a12;
    /* `shadow.dark.elevation.400.menu`. The two inset layers are the half that
       does not survive being flattened: a dark panel on a dark canvas
       disappears without a lit top edge, which is what the white insets are. */
    --shadow-panel:
      0 2px 5px rgba(0, 0, 0, 0.35),
      0 10px 16px rgba(0, 0, 0, 0.35),
      inset 0 0 0.5px rgba(255, 255, 255, 0.35),
      inset 0 0.5px 0 rgba(255, 255, 255, 0.08);
    --checker-a: #414753;
    --checker-b: #2e323b;
    --swatch-ring: rgba(255, 255, 255, 0.2);
    --art-ink: #e2e6ee;
    --art-wash-a: #403396;
    --art-wash-b: #2d236d;
    --art-accent: #006f4a;
    /* Lowered for the dark theme. A stroke tuned for dark-on-light blooms when
       it is light-on-dark: the same icon reads a size larger than the text
       beside it. Phosphor has no grade axis, so this is opacity — it takes the
       bloom off without changing the glyph's size, which is the property that
       mattered about GRAD. */
    --icon-ink: 0.88;
  }
}

:root[data-theme='light'] {
  /* Surfaces. `bg.default` is the panel; `bg.secondary` is the canvas under
     it, which is what makes the artwork sit *on* something rather than bleed
     into the chrome. */
  --bg: #ffffff;
  --bg-sunken: #f1f3f8;
  --panel: #ffffff;
  --border: #e2e6ee;
  /* `ramp.grey.300`. UI3 names one border colour per context and no "strong"
     one, so the emphasis step comes from the ramp the roles are built on
     rather than from a colour picked by eye. */
  --border-strong: #cad0db;
  /* Ink at an opacity, which is the whole of UI3's grey system: `text.default`
     is #0f1216 at 90%, not a flattened grey. That is why a secondary label
     stays right when it lands on the canvas, on a card, or on a selected row —
     a flattened grey is correct on exactly one surface. */
  --text: rgba(15, 18, 22, 0.9);
  --text-muted: rgba(15, 18, 22, 0.5);
  --text-subtle: rgba(15, 18, 22, 0.3);
  --accent: #847bff;
  --accent-soft: #f1f2ff;
  --on-accent-soft: #5a4fd6;
  /* The accent as a **fill under white**, which `--accent` itself does not
     carry: `#ffffff` on `#847bff` measures 3.36:1, and that is the label on
     every primary button in the product. Same reasoning as `--on-accent-soft`
     above and the same ramp step — `--accent` stays UI3's `color.bg.brand`,
     because `ui3.test.ts` holds that decision and it is the right one; what is
     ours is the pairing, so the new token is ours too. */
  --accent-fill: #5a4fd6;
  --on-accent: #ffffff;
  --on-danger: #ffffff;
  /* Components. UI3 has no role for "this is a component" — it is our concept,
     not Figma's UI kit's — so it is the purple ramp's own deeper step: related
     to the brand, and never the same colour as a selection. */
  --component: #6b5ae8;
  --component-soft: #e2e4ff;
  --hover: rgba(15, 18, 22, 0.1);
  --field: #f1f3f8;
  --field-hover: #e7eaf2;
  --field-border: transparent;
  --danger: #ca3b3e;
  --danger-soft: #ffddda;
  --positive: #008c5f;
  --warning: #9b6c00;
  /* The soft companion, for a banner rather than a word. Beside `--danger-soft`
     and for the same reason: a warning painted as text on the panel colour is a
     sentence people read past. */
  --warning-soft: #fdf1d6;
  /* `shadow.light.elevation.400.menu`, all three of its layers. A panel's
     shadow in UI3 is a stack — a tight contact shadow, a wide ambient one and
     a hairline doing the work of a 1px border — and flattening it to one blur
     is what makes a floating panel look pasted on. */
  --shadow-panel:
    0 2px 5px rgba(0, 0, 0, 0.15),
    0 10px 16px rgba(0, 0, 0, 0.12),
    0 0 0.5px rgba(0, 0, 0, 0.12);

  /* The transparency checkerboard, under a swatch or a gradient stop. */
  --checker-a: #ffffff;
  --checker-b: #e2e6ee;
  /* `ramp.black.300`: the hairline round a swatch, so a pale colour still has
     an edge. */
  --swatch-ring: rgba(15, 18, 22, 0.2);

  /* The template thumbnails in the file browser, drawn in CSS. */
  --art-ink: #1d2027;
  --art-wash-a: #e2e4ff;
  --art-wash-b: #f1f2ff;
  --art-accent: #c3f4da;
  /* Back to full ink: someone who has chosen the light theme on a machine set
     to dark must not inherit the dark theme's icon correction. Each
     `[data-theme]` block lists everything it decides, which is what makes
     the two orderings independent of each other. */
  --icon-ink: 1;
}

:root[data-theme='dark'] {
  --bg: #2e323b;
  /* `ramp.grey.900`, which is what UI3 calls `bg.menu` — its darkest named
     surface. The canvas has to sit *below* the panels in the dark theme too,
     and UI3's dark `bg.secondary` resolves to the same value as
     `bg.default`, so it cannot be that. */
  --bg-sunken: #1d2027;
  --panel: #2e323b;
  --border: #414753;
  --border-strong: #6e7687;
  --text: #ffffff;
  --text-muted: rgba(255, 255, 255, 0.7);
  --text-subtle: rgba(255, 255, 255, 0.4);
  --accent: #847bff;
  --accent-soft: #403396;
  --on-accent-soft: #b0a8ff;
  --accent-fill: #6b60e0;
  --on-accent: #ffffff;
  --on-danger: #ffffff;
  --component: #a8a8ff;
  --component-soft: #403396;
  --hover: rgba(255, 255, 255, 0.1);
  --field: #3a404b;
  --field-hover: #434a57;
  --field-border: transparent;
  --danger: #ff8d87;
  --danger-soft: #811b1f;
  --positive: #5fcb9a;
  --warning: #f4c781;
  --warning-soft: #4a3a12;
  /* `shadow.dark.elevation.400.menu`. The two inset layers are the half that
     does not survive being flattened: a dark panel on a dark canvas
     disappears without a lit top edge, which is what the white insets are. */
  --shadow-panel:
    0 2px 5px rgba(0, 0, 0, 0.35),
    0 10px 16px rgba(0, 0, 0, 0.35),
    inset 0 0 0.5px rgba(255, 255, 255, 0.35),
    inset 0 0.5px 0 rgba(255, 255, 255, 0.08);
  --checker-a: #414753;
  --checker-b: #2e323b;
  --swatch-ring: rgba(255, 255, 255, 0.2);
  --art-ink: #e2e6ee;
  --art-wash-a: #403396;
  --art-wash-b: #2d236d;
  --art-accent: #006f4a;
  /* Lowered exactly as the media-query dark block lowers it. The two dark
     blocks each list every token they change — see the note at the top of this
     file — so the icon correction is no more exempt from that than a colour
     is. */
  --icon-ink: 0.88;
}

* {
  box-sizing: border-box;
}

/**
 * `hidden` means hidden, whatever the element's own rule says.
 *
 * The browser hides `[hidden]` with `display: none` at user-agent priority,
 * which **any** author rule beats — so a panel whose own rule says
 * `display: flex` is on screen with `element.hidden = true` set on it, and the
 * code that set it is correct and does nothing. That is a bug with no error, no
 * failing assertion in Node (the harness has no cascade at all, lesson 28) and
 * no symptom except a panel that will not go away.
 *
 * It was found twice in one afternoon: the plan board covered the canvas on
 * every page, and the timeline strip left a hairline border above the status
 * bar in files with no animation in them. Both were `display:` in the class's
 * own rule. One line here ends the class rather than one guard per panel, and
 * `stylesheet.test.ts` refuses a build without it.
 */
[hidden] {
  display: none !important;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  font-family: var(--font);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* Visible focus for every interactive element. Never `outline: none`. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: 3px;
}

/* ---------------------------------------------------------------- toolbar */

.toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  height: var(--toolbar-height);
  padding: 0 12px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}

/* The mark is the application menu. It reads as a wordmark and behaves as a
   button, which is the arrangement every design tool without an OS menu bar
   has arrived at. */
.brand {
  font-weight: 700;
  letter-spacing: -0.3px;
  font-size: 14px;
  padding: 4px 8px 4px 4px;
  margin-right: 2px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
}

.brand:hover,
.brand:focus-visible {
  background: var(--hover);
}

/* --- How much of the interface is on screen --------------------------------

   Two states beyond normal. `minimal` is Figma's "Minimize UI": both side rails
   go, and what is left is the top toolbar — the file name, the minimize button
   that brings the rails back, the collaborators and Share — and the bottom tool
   dock, so the canvas has the whole width and the file is still identifiable and
   still drawable in. `hidden` is the presentation state and leaves the canvas
   alone with the status bar.

   The button that restores the rails lives in the toolbar precisely because
   `minimal` keeps it: a minimize that hid its own way out would be a one-way
   door once the shortcut was forgotten.

   Panels are hidden with `display: none` rather than moved off screen: an
   off-screen panel keeps re-rendering on every document change, which is work
   nobody can see — the same reason the sidebar tabs unmount rather than hide. */
.ui-minimal .sidebar,
.ui-minimal .panel-rail {
  display: none;
}

.ui-hidden .sidebar,
.ui-hidden .toolbar,
.ui-hidden .tool-dock {
  display: none;
}

/* Interface scale, applied to the chrome and never to the canvas. Scaling the
   canvas host would change the relationship between a CSS pixel and a design
   pixel, so "make the interface bigger" would silently zoom the artwork — and
   every pointer coordinate would need the factor divided back out. */
.toolbar,
.sidebar,
.tool-dock,
.status-bar,
.context-menu {
  zoom: var(--ui-scale, 1);
}

.tool-group {
  display: flex;
  align-items: center;
  gap: 2px;
}

.spacer {
  flex: 1 1 auto;
}

/* ---------------------------------------------------------------------------
   The tool dock — a floating pill over the canvas.

   Tools live at the bottom centre rather than in the top bar for a reason
   beyond fashion: the pointer spends its time on the artwork, and the bottom
   centre is a shorter trip from anywhere on the canvas than the top-left
   corner. It also frees the top bar to be about the *document* — name, share,
   zoom — instead of mixing document and drawing controls in one strip.
   --------------------------------------------------------------------------- */

.tool-dock {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 6px;
  border-radius: 14px;
  background: var(--dock);
  color: var(--dock-text);
  box-shadow:
    0 10px 32px rgba(0, 0, 0, 0.28),
    0 1px 2px rgba(0, 0, 0, 0.3);
  /* Above the canvas, below any dialog or the command palette. */
  z-index: 20;
}

/* The cells live in their own element so the whole set can be swapped when the
   page kind changes; `display: contents` means the row is still one flex line
   and nothing about the layout moves. */
.tool-dock-cells {
  display: contents;
}

/* ---------------------------------------------------------------------------
   The dock on a riff.

   A board's dock is light on a light surface, which is what FigJam does and is
   not only fashion: the dark pill is the design tool's own furniture, and a
   board is a room people are meant to draw on rather than an instrument panel.
   The tools themselves are a different *set*, not a subset — see
   `RIFF_TOOL_GROUPS`.
   --------------------------------------------------------------------------- */
.tool-dock.is-riff {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-panel);
}

.tool-dock.is-riff .tool-button,
.tool-dock.is-riff .tool-caret,
.tool-dock.is-riff .dock-actions,
.tool-dock.is-riff .dock-templates {
  color: var(--text-muted);
}

.tool-dock.is-riff .tool-button:hover,
.tool-dock.is-riff .tool-caret:hover,
.tool-dock.is-riff .dock-actions:hover,
.tool-dock.is-riff .dock-templates:hover {
  background: var(--hover);
  color: var(--text);
}

/* The active tool keeps the accent it has in the dark dock: it is the same
   statement — "this is what the pointer is holding" — and two different
   answers to it would read as two different controls. */
.tool-dock.is-riff .tool-button.active,
.tool-dock.is-riff .tool-cell.is-active .tool-caret {
  background: var(--accent-fill);
  color: var(--on-accent);
}

.tool-dock.is-riff .dock-divider {
  background: var(--border);
}

.tool-dock .dock-divider {
  width: 1px;
  height: 22px;
  margin: 0 6px;
  background: rgba(255, 255, 255, 0.16);
  flex: none;
}

/* The Actions button is not a tool — it opens a list — so it is not a
   `.tool-button`: the dock counts those, and a control that is not a tool
   sitting in that count is how "every tool has a button" stops being true
   without anything failing. It borrows the size and the states. */
.dock-actions {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--dock-muted);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.dock-actions:hover {
  background: var(--dock-hover);
  color: var(--dock-text);
}

.dock-actions .icon,
.dock-mode .icon {
  font-size: 20px;
}

/* ---------------------------------------------------------------------------
   The mode switch, at the right-hand end of the dock.

   Figma's control, and it replaces four text tabs that lived in the inspector's
   heading. Icons rather than words for a reason that is measured rather than
   aesthetic: the tab strip was 282px in English and 334px in Russian against a
   264px heading (bug 465), and four glyphs are the same width in every
   language. The names are in the tooltip and the accessible name, because a row
   of unlabelled glyphs is a control you have to press to learn (lesson 22).
   --------------------------------------------------------------------------- */
.dock-modes {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.06);
}

.dock-mode {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--dock-muted);
  cursor: pointer;
}

.dock-mode:hover {
  background: var(--dock-hover);
  color: var(--dock-text);
}

.dock-mode.is-active {
  background: var(--dock-hover);
  color: var(--dock-text);
}

.tool-dock.is-riff .dock-modes {
  background: var(--bg-sunken);
}

.tool-dock.is-riff .dock-mode {
  color: var(--text-muted);
}

.tool-dock.is-riff .dock-mode:hover,
.tool-dock.is-riff .dock-mode.is-active {
  background: var(--hover);
  color: var(--text);
}

/* ---------------------------------------------------------------------------
   The dock in Dev Mode.

   Green, which is Figma's colour for it and is doing a job rather than being
   decoration: this is the one row in the interface whose *contents* change with
   the mode, and a row that swapped its tools without changing its appearance
   reads as tools having gone missing. The palette's own success green, so it is
   a token this file already keeps against the contrast floor rather than a
   fifth accent nobody checks.
   --------------------------------------------------------------------------- */
.tool-dock.is-dev .tool-button.active,
.tool-dock.is-dev .tool-cell.is-active .tool-caret {
  background: var(--positive);
  color: #fff;
}

.tool-dock.is-dev .dock-mode[data-mode='dev'] {
  color: var(--positive);
}

.tool-button {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--dock-muted);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.tool-button:hover {
  background: var(--dock-hover);
  color: var(--dock-text);
}

.tool-button.active {
  background: var(--accent-fill);
  color: var(--on-accent);
}

/* The dock is the one place the icon is the *only* thing identifying a
   control — every other icon in the app sits next to a label or in a row whose
   context gives it away. It gets the largest size for that reason. */
.tool-button .icon {
  font-size: 22px;
}

/* ---------------------------------------------------------------------------
   Tool groups and their flyouts.

   Figma's bar is groups rather than buttons because the shapes are
   alternatives to one another: you are picking *a shape*, and the one you
   picked last is nearly always the one you want next. So the group shows its
   most recent member and a small caret opens the rest — one click for the
   common case, two for the other four.
   --------------------------------------------------------------------------- */
.tool-cell {
  position: relative;
  display: flex;
  align-items: center;
  border-radius: 9px;
}

/* A split button: the tool takes the left of the pill, the chevron the right,
   and the two share one rounded outline so it reads as one control with two
   halves rather than as two buttons that happen to touch. */
.tool-cell.has-caret .tool-button {
  width: 32px;
  border-radius: 9px 0 0 9px;
}

.tool-caret {
  display: grid;
  place-items: center;
  width: 16px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 0 9px 9px 0;
  background: transparent;
  color: var(--dock-muted);
  font-size: var(--type-body-small);
  letter-spacing: var(--type-body-small-tracking);
  line-height: 1;
  cursor: pointer;
}

.tool-caret .icon {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* Hovering either half lights both, because pressing either one is a way of
   getting at the same group. */
.tool-cell.has-caret:hover .tool-button,
.tool-cell.has-caret:hover .tool-caret {
  background: var(--dock-hover);
  color: var(--dock-text);
}

.tool-caret:hover,
.tool-caret[aria-expanded='true'] {
  background: var(--dock-hover);
  color: var(--dock-text);
}

/* The active pill: both halves take the accent, so the chevron does not sit
   as a dark notch cut out of a blue button.
   `color-mix` rather than `rgba(255, 255, 255, 0.75)`: the ink on the accent is
   white in the light theme and near-black in the dark one, so a fixed white at
   75% is a chevron that disappears in one of them. Mixing the ink with the
   surface it is on dims it in whichever direction is correct. */
.tool-cell.is-active .tool-caret,
.tool-cell.is-active:hover .tool-caret {
  background: var(--accent-fill);
  color: color-mix(in srgb, var(--on-accent) 75%, var(--accent));
}

.tool-cell.is-active .tool-caret:hover {
  color: var(--on-accent);
}

.tool-flyout {
  position: absolute;
  bottom: calc(100% + 8px);
  left: -4px;
  /* Wide enough for the longest row — "Place image…" with ⌃⇧K beside it —
     because a menu whose labels wrap is a menu that looks broken. */
  min-width: 214px;
  padding: 5px;
  border-radius: 11px;
  background: var(--dock);
  color: var(--dock-text);
  box-shadow:
    0 10px 32px rgba(0, 0, 0, 0.34),
    0 1px 2px rgba(0, 0, 0, 0.3);
  /* Above the dock it belongs to, and above the canvas — but still below a
     dialog, which is the only thing entitled to cover it. */
  z-index: 25;
}

.tool-flyout-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  height: 30px;
  padding: 0 9px 0 7px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--dock-muted);
  font-family: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.tool-flyout-item:hover,
.tool-flyout-item:focus-visible {
  background: var(--dock-hover);
  color: var(--dock-text);
}

.tool-flyout-item.is-on {
  color: var(--dock-text);
}

.tool-flyout-item .icon {
  font-size: 16px;
  width: 18px;
  text-align: center;
}

.tool-flyout-name {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
}

/* The shortcut is a reminder, not a label: dimmed and right-aligned, so the
   eye reads the names down the left and finds the key only when looking. */
.tool-flyout-key {
  flex: 0 0 auto;
  color: var(--dock-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  opacity: 0.75;
}

/* The zoom readout rides in the dock, where the eye already is. */
.dock-zoom {
  min-width: 54px;
  height: 38px;
  padding: 0 8px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--dock-muted);
  font: inherit;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.dock-zoom:hover {
  background: var(--dock-hover);
  color: var(--dock-text);
}

/* `min-height`, not `height`. A label long enough to wrap — "Create a set from
   current tokens" in a 264px panel — overflowed a fixed 28px box and printed
   its second line outside the button's own border. A minimum keeps every
   single-line button exactly as it was and lets the rare wrapping one grow,
   which is why the padding is on both axes now. */
.labelled-button {
  min-height: 28px;
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font-size: 12px;
  font-family: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}

.labelled-button:hover:not(:disabled) {
  background: var(--bg-sunken);
}

.labelled-button:disabled {
  opacity: 0.4;
  cursor: default;
}

.file-button input[type='file'] {
  display: none;
}

/* ------------------------------------------------------------------- body */

.body {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}

/* The widths come from custom properties the splitters write, so the drag
   supplies a number and the stylesheet keeps ownership of the layout. */
.sidebar {
  background: var(--panel);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* The handle is absolutely positioned against this. */
  position: relative;
}

.sidebar.left {
  width: var(--sidebar-left-width, var(--sidebar-width));
  flex: 0 0 var(--sidebar-left-width, var(--sidebar-width));
  border-right: 1px solid var(--border);
}

.sidebar.right {
  width: var(--sidebar-right-width, 264px);
  flex: 0 0 var(--sidebar-right-width, 264px);
  border-left: 1px solid var(--border);
}

/* ---------------------------------------------------------------- splitters

   Six pixels wide, with a one-pixel line drawn inside on hover. A handle only
   as wide as the line it draws is a handle nobody can grab: the target has to
   be bigger than the thing it looks like, which is why this is a transparent
   strip that straddles the border rather than the border itself. */
.splitter {
  position: absolute;
  z-index: 5;
  background: transparent;
}

.splitter::after {
  content: '';
  position: absolute;
  inset: 0;
  background: transparent;
  transition: background 100ms linear;
}

/* The divider itself, which the splitter did not draw.
 *
 * A resize handle is invisible until you hover it, and that is right for a
 * handle — but it was also the only thing between the pages list and the panel
 * below it, so two unrelated regions ran together into one column and the
 * boundary appeared only while the pointer was on it. A hairline at rest is
 * what makes them read as two panels; the accent band above still takes over
 * on hover, because `::after` is painted after this and covers it. */
.splitter::before {
  content: '';
  position: absolute;
  background: var(--border);
}

.splitter-horizontal::before {
  top: 0;
  bottom: 0;
  left: 2px;
  width: 1px;
}

.splitter-vertical::before {
  left: 0;
  right: 0;
  top: 2px;
  height: 1px;
}

.splitter:hover::after,
.splitter:focus-visible::after,
body.resizing-horizontal .splitter-horizontal:active::after,
body.resizing-vertical .splitter-vertical:active::after {
  background: var(--accent);
}

.splitter:focus-visible {
  outline: none;
}

.splitter-horizontal {
  top: 0;
  bottom: 0;
  width: 6px;
  cursor: col-resize;
}

.splitter-horizontal::after {
  left: 2px;
  right: 2px;
}

.splitter-vertical {
  left: 0;
  right: 0;
  height: 6px;
  cursor: row-resize;
}

.splitter-vertical::after {
  top: 2px;
  bottom: 2px;
}

/* `end` is the right edge for a horizontal handle and the bottom for a
   vertical one; `start` is the mirror. */
.splitter-horizontal.splitter-end { right: -3px; }
.splitter-horizontal.splitter-start { left: -3px; }
.splitter-vertical.splitter-end { bottom: -3px; }
.splitter-vertical.splitter-start { top: -3px; }

/* During a drag the cursor and the no-select apply to the whole window, or
   dragging across the layer tree highlights every row it passes. */
body.resizing-horizontal,
body.resizing-horizontal * {
  cursor: col-resize !important;
  user-select: none;
}

body.resizing-vertical,
body.resizing-vertical * {
  cursor: row-resize !important;
  user-select: none;
}

.sidebar-heading {
  height: 32px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-subtle);
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}

.panel-body {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.canvas-host {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  background: var(--bg-sunken);
}

/* The panels float over a sunken canvas, Pixso-style, rather than butting
   against it — the seam is a shadow, not a hard line. */
.sidebar.left {
  box-shadow: 1px 0 0 var(--border);
}

.sidebar.right {
  box-shadow: -1px 0 0 var(--border);
}

.flowi-scene,
.flowi-overlay {
  position: absolute;
  inset: 0;
  display: block;
}

.flowi-overlay {
  /* The overlay receives all pointer input; the scene canvas never does. */
  touch-action: none;
}

/* The focus target input methods attach to, parked under the text caret.

   Every property here is load-bearing, and most are the *opposite* of what
   "hide this element" usually means. `display: none`, `visibility: hidden` and
   the `hidden` attribute all make an element unfocusable, and an unfocusable
   proxy means no composition events at all — no Japanese, Chinese or Korean
   input and no dead-key accents — with nothing on screen to suggest why. So it
   is transparent and one pixel wide, but it is laid out, focusable, and
   positioned at the caret: the candidate window is placed relative to it, and a
   proxy parked at the origin opens the candidate list in the corner of the
   viewport rather than beside the word being typed. `pointer-events: none`
   stops it stealing clicks meant for the canvas underneath. */
.flowi-ime-proxy {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: 0;
  border: 0;
  outline: none;
  resize: none;
  overflow: hidden;
  opacity: 0;
  z-index: 1;
  pointer-events: none;
  background: transparent;
  color: transparent;
  /* Some platforms size the candidate window from the focused element's font;
     matching the UI default keeps it from opening at 1px type. */
  font: 13px ui-sans-serif, system-ui, sans-serif;
}

/* ----------------------------------------------------------------- layers */

.layer-list {
  padding: 4px 0;
}

.layer-row {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 26px;
  padding-right: 8px;
  /* The hover and selection fills are rounded, matching the page row above it:
     the owner asked for the whole row to light up on hover the way a page row
     does, and a square full-bleed band reads as a stripe across the panel
     rather than as "this row is the thing under the pointer". The `:hover`
     background has always covered the whole row (it is on `.layer-row`, not on
     the twisty); the radius is what makes the two lists look like one control. */
  border-radius: var(--radius);
  cursor: default;
  user-select: none;
  color: var(--text);
}

.layer-row:hover {
  background: var(--bg-sunken);
}

/* Hovered from the canvas. The same background as `:hover`, because it is the
   same state — the pointer just happens to be over the object rather than over
   the row. Kept below `.selected` so a selected row stays reading as selected. */
.layer-row.is-hovered {
  background: var(--bg-sunken);
}

.layer-row.selected,
.layer-row.selected.is-hovered {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

.layer-row.hidden-layer .layer-name {
  opacity: 0.45;
}

.twisty,
.twisty-spacer {
  width: 14px;
  flex: 0 0 14px;
  min-width: 0;
  border: none;
  background: none;
  color: var(--text-subtle);
  font-size: var(--type-body-small);
  letter-spacing: var(--type-body-small-tracking);
  cursor: pointer;
  padding: 0;
}

/* Sized for a real icon, not for a box-drawing character. The old 14px slot at
   11px type was chosen for `▢`, which the system font drew hairline and small;
   an icon-font glyph at the app's own size needs the room and gets the weight
   right for free. */
.layer-icon {
  width: 18px;
  flex: 0 0 18px;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
}

.layer-icon .icon {
  font-size: 16px;
}

/* Components and their instances are the one place in the tree where the icon
   carries meaning beyond the shape — they are worth finding at a glance. The
   two share this colour and differ by the glyph: a cluster of four diamonds is
   the main component, one hollow diamond is an instance of it. */
.component-icon,
.instance-icon {
  color: var(--component);
}

/* The whole row, not only the glyph. Scanning a tree means reading names, and
   a name that is purple answers "is this a component" without the eye having
   to travel to the icon and back. Selection still wins — a selected row is
   blue, because what is selected outranks what kind of thing it is. */
.layer-row.is-component .layer-name,
.layer-row.is-instance .layer-name {
  color: var(--component);
}

.layer-row.selected.is-component .layer-name,
.layer-row.selected.is-instance .layer-name {
  color: var(--accent);
}

.layer-row.selected .layer-icon {
  color: var(--accent);
}

.layer-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}

.layer-actions {
  display: none;
  gap: 2px;
}

.layer-row:hover .layer-actions {
  display: flex;
}

/* The button gets a hover of its own, on top of the row's.
 *
 * The row lights up with `--bg-sunken` on hover, and the generic `.icon-button`
 * hover is *also* `--bg-sunken` — so pointing at the eye or the lock over an
 * already-hovered row changed nothing, and the control read as dead. The owner
 * asked for the right-hand button to have its own hover. `--hover` is a
 * translucent overlay rather than an opaque fill, so it darkens the row's
 * sunken background instead of matching it, and the button stands out whether
 * the row is merely hovered or selected. */
.layer-actions .icon-button:hover {
  background: var(--hover);
}

/* -------------------------------------------------------------- inspector */

.panel-section {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}

/* The row above Position: the layer's name, and the buttons that change what
   kind of thing it is. Not a `.panel-section` — it has no heading, because the
   name *is* the heading. */
.inspector-title {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}

.inspector-name {
  flex: 1 1 auto;
  /* `min-width: auto` on a flex item resolves to an input's min-content size,
     about 155px, and silently beats the basis — see the note on the field
     rules. Every fixed-basis input in this file resets it for that reason. */
  min-width: 0;
  height: 26px;
  padding: 0 8px;
  border: 1px solid transparent;
  border-radius: 5px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  text-overflow: ellipsis;
}

.inspector-name:hover {
  border-color: var(--border);
}

.inspector-name:focus {
  border-color: var(--accent);
  background: var(--bg);
  outline: none;
}

.inspector-title-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
}

/* The heading is a row: title on the left, the section's own actions on the
   right. A fixed landmark, so "where do I add another one?" has the same answer
   however long the list already is. */
.section-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  min-height: 20px;
}

/* Sentence case, at the size of a heading, in the colour of text.
   It was 11px uppercase grey with letter-spacing, which is a *caption* — and
   the panel then had nothing that looked like a heading at all, so "Position"
   and the word "Alignment" under it read as two labels of equal weight. The
   reference gives the section its own line and its own size, and the captions
   inside it stay small and grey; the difference between the two is what makes
   the panel scannable without reading. */
.panel-section h3 {
  margin: 0;
  font-size: var(--type-body-large);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}

.section-actions {
  display: flex;
  gap: 2px;
  flex: 0 0 auto;
}

/* The actions stay quiet until the section is under the pointer — a row of
   permanently-lit buttons beside every heading competes with the values, which
   are what the panel is for. They remain focusable throughout, so the keyboard
   is not affected by a hover rule. */
.section-actions .icon-button {
  min-width: 20px;
  min-height: 20px;
  opacity: 0.55;
}

.panel-section:hover .section-actions .icon-button,
.section-actions .icon-button:focus-visible,
.section-actions .icon-button.is-on {
  opacity: 1;
}

.section-actions .icon-button.is-on {
  color: var(--accent);
}

.field {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
  min-width: 0;
}

.field-label {
  flex: 0 0 62px;
  /* Same trap as the number fields: without this, a label longer than 62px is
     never allowed to shrink to its basis, so "Corner radius" wrapped onto two
     lines and pushed its own field down with it. */
  min-width: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

/* Filled, not outlined — see `--field` at the top of this file. The border is
   still 1px so that revealing it on hover moves nothing; only its colour
   changes. */
.field input,
.field select,
.field textarea {
  flex: 1 1 auto;
  min-width: 0;
  height: 26px;
  padding: 0 7px;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  background: var(--field);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
}

.field input:hover,
.field select:hover,
.field textarea:hover {
  background: var(--field-hover);
  border-color: var(--border);
}

.field textarea {
  height: auto;
  padding: 5px 7px;
  resize: vertical;
  font-family: inherit;
}

.field input[type='checkbox'] {
  flex: 0 0 auto;
  height: auto;
  width: auto;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--accent);
}

/* In the inspector a field's label goes *above* its control, never beside it.
   Figma has no left-hand labels at all, and the reason is width: at 264px a
   62px label column leaves 190px for a value, and the two-column rows people
   actually use — X and Y, opacity and radius — then have 91px each. Stacking
   was already the rule inside `.field-pair` (see below); this makes it the rule
   for the panel, so a lone field and a paired one line up.

   A checkbox keeps its label beside it, because there the label *is* the row. */
#inspector-panel .field:not(:has(input[type='checkbox'])),
#prototype-panel .field:not(:has(input[type='checkbox'])) {
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
}

#inspector-panel .field:not(:has(input[type='checkbox'])) > .field-label,
#prototype-panel .field:not(:has(input[type='checkbox'])) > .field-label {
  flex: 0 0 auto;
}

/* A captioned group: one small grey word over a row of controls. The margin is
   on the group rather than on its children so that two groups in a section are
   spaced from each other and the caption stays welded to what it captions. */
.field-group {
  margin-bottom: 12px;
}

.field-group:last-child {
  margin-bottom: 0;
}

/* The caption, and whatever belongs to the group rather than to one field —
   the keyframe diamond for a row too tight to carry it (bug 495). */
.group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
}

.group-head > .group-label {
  margin-bottom: 0;
}

.group-head {
  margin-bottom: 6px;
}

.group-label {
  display: block;
  margin-bottom: 6px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

/* Inside a captioned group the field's own label is already said above it. */
.field-group > .field > .field-label:empty {
  display: none;
}

/* Three mutually exclusive choices, all visible, one lit. Equal columns rather
   than content width: the three are alternatives, and a row where one button is
   wider than the others reads as one of them being the important one. */
.segmented {
  display: flex;
  gap: 1px;
  padding: 1px;
  border-radius: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
}

.segmented .segment {
  flex: 1 1 0;
  min-width: 0;
  height: 24px;
  border-radius: 5px;
  border: 1px solid transparent;
}

.segmented .segment:hover {
  background: var(--hover);
}

/* The selected one carries a border and a raised surface rather than the accent
   colour: an accent-filled segment competes with the accent used for *bound to
   a token* two rows below, and two different meanings on one colour in one
   panel is how a person learns to distrust both. */
.segmented .segment.is-on {
  background: var(--panel);
  border-color: var(--border);
  color: var(--text);
}

/* A number and the mode that decides whether the number is yours to type,
   side by side. The select is narrow on purpose: it holds one short word, and
   the digits are what a person reads. */
.axis-field {
  display: flex;
  align-items: stretch;
  gap: 4px;
  min-width: 0;
}

.axis-field > .field:first-child {
  flex: 1 1 auto;
  min-width: 0;
  margin-bottom: 0;
}

/*
 * The mode used to be a 46px `<select>` beside the number — two boxes for one
 * answer, and its own label clipped often enough that this file carried a note
 * about it. It is inside the box now (`.axis-mode`), which is Figma's shape and
 * what the owner asked for: the word when the size is computed, a chevron when
 * it is not, and the whole right-hand end opens the menu either way.
 */

/* The number has to be allowed to be narrow. An input's default `min-width` is
   its content width, which beats every flex basis in this file — the note on
   `.inspector-name` is about the same trap — and here it made "922" render as
   "92", which reads as the wrong value rather than as a clipped one. */
.axis-field .number-input,
.axis-field .number-input input {
  min-width: 0;
}

/*
 * An approval that has ended, on the connections screen.
 *
 * Dimmed rather than removed, and dimmed rather than struck through: the row
 * is there for the sentence explaining *why* a connection stopped, and a
 * strike-through reads as "deleted" — which is the reading that made the
 * previous behaviour (hide it entirely) look reasonable for four updates.
 */
.server-file.is-ended {
  opacity: 0.62;
}

/*
 * The sizing mode, inside the field.
 *
 * A button and never a label with an arrow beside it: the word `Hug` is the
 * control, so it has to be the thing that takes the press. It is sized from
 * its content — `Fill` and `Hug` are four letters, the chevron is an icon —
 * with a floor so the caret has somewhere to sit, and it never shrinks the
 * number below legibility because the number's own `min-width: 0` above lets
 * the input give way first.
 */
.axis-mode {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  min-width: 18px;
  height: 100%;
  padding: 0 4px;
  border: none;
  border-radius: 0 4px 4px 0;
  background: none;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1;
  cursor: pointer;
}

.axis-mode:hover {
  color: var(--text);
  background: var(--hover);
}

.axis-mode:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
}

/* The word is the state, so it reads as text rather than as a dimmed hint —
   this is the half the report was about: "они подсвечиваются в инпуте".

   `max-content` on the button, not a fixed width: `Fill`, `Hug`, «Обтек.» and
   «Запол.» are four different widths, and a number picked for one of them
   clips another in a language nobody building it reads. The number beside it
   is the flexible half — its own `min-width: 0` is what lets it give way. */
.axis-mode:not(.is-caret) {
  min-width: max-content;
}

.axis-mode-word {
  color: var(--text);
  white-space: nowrap;
}

/* The chevron is smaller than a toolbar icon, and narrower than the word: it
   is a hint that there is a menu, not a control competing with the number
   beside it — and the number is what the field is for. */
.axis-mode.is-caret {
  min-width: 14px;
  padding: 0 2px;
}

.axis-mode.is-caret .icon-glyph,
.axis-mode.is-caret .icon-fallback {
  font-size: 12px;
}

/* Room for it, and the input gives the room. Without this the box grows and
   the two axes stop lining up with every other pair in the panel. */
.number-input.has-trailing input {
  min-width: 0;
}

/* Hug and fill compute the number. Dimmed rather than disabled: it is still
   the value, still readable and still selectable, and typing into it is a
   legitimate way of saying "make it fixed at this". */
.axis-field .is-computed input {
  color: var(--text-muted);
}

.field-pair {
  display: flex;
  gap: 8px;
}

/* Inside a pair the label goes *above* its field rather than beside it.
   Side by side, a two-word label ("Corner radius", "Bottom right") eats most of
   a half-width column and leaves the input too narrow to show two digits — so
   `24` rendered as `2`, which reads as the wrong value rather than as a clipped
   one. That is the worst kind of layout bug: it lies about the document.
   Stacking is also what Figma does, for the same reason. */
.field-pair .field {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  flex: 1 1 0;
  margin-bottom: 6px;
}

.field-pair .field-label {
  flex: 0 0 auto;
  min-width: 0;
}

/* A checkbox needs no room to speak of, so its label takes the rest rather
   than being squeezed into 62px and wrapping "Clip content" onto two lines. */
.field input[type='checkbox'] {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  order: -1;
}

.field:has(input[type='checkbox']) .field-label {
  flex: 1 1 auto;
}

.field.readonly .field-value {
  flex: 1 1 auto;
  font-family: var(--mono);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-subtle);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.color-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}

/* The swatch that opens the picker.

   Chequerboard behind the colour, because a fill at 20% opacity over a solid
   panel background looks like a *pale* colour rather than a transparent one —
   and the difference matters more here than anywhere else in the inspector. */
/* An image fill: swatch, scale mode, opacity, and the two buttons — one row,
   the same shape as the solid and gradient rows above it, so a fill list of
   mixed kinds still reads as a list rather than as three different controls. */
.paint-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}

.paint-row .field {
  flex: 1 1 auto;
  min-width: 0;
}

/* The image swatch carries a word rather than a colour, so it needs to be
   legible at 26px — and it is not a button, unlike its solid-fill neighbour. */
.paint-row .color-swatch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  cursor: default;
}

.color-swatch {
  width: 26px;
  height: 26px;
  flex: 0 0 26px;
  min-width: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  cursor: pointer;
  background-color: var(--checker-a);
  background-image:
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%),
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
  position: relative;
  overflow: hidden;
}

.color-swatch::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--swatch-color, #000);
  opacity: var(--swatch-alpha, 1);
}

.color-swatch[aria-expanded='true'] {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

/* --------------------------------------------------------------- gradients

   The gradient fill *row* is the same shape as a solid one — swatch, opacity,
   eye, minus — and everything that used to live inline in the sidebar now
   lives in the popover the swatch opens. */

.color-swatch.is-gradient::after {
  background: var(--gradient, #000);
  opacity: 1;
}

.gradient-label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------- the gradient editor, in the
   colour popover */

.color-popover.is-gradient {
  width: 268px;
}

.cp-pane {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.cp-pane[hidden] {
  display: none;
}

.cp-tabs {
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: 7px;
  background: var(--bg);
}

.cp-tab {
  flex: 1 1 0;
  min-width: 0;
  height: 24px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.cp-tab.is-on {
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}

.cp-gradient {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cp-gradient-head {
  display: flex;
  align-items: center;
  gap: 6px;
}

.cp-gradient-kind {
  flex: 1 1 auto;
  min-width: 0;
  height: 26px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.cp-gradient-angle {
  flex: 0 0 52px;
  min-width: 0;
  height: 26px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: center;
}

/* The ramp draws the same CSS gradient the browser would, over the same
   chequerboard as a solid swatch — a gradient fading to transparent has to
   look like it fades to transparent, not like it fades to white.

   `position: relative` is load-bearing: the chips are positioned against it,
   and without it they would place themselves against the page. */
.cp-ramp {
  position: relative;
  height: 32px;
  margin-top: 12px;
  border-radius: 6px;
  cursor: copy;
  touch-action: none;
  background-color: var(--checker-a);
  background-image:
    var(--gradient, none),
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%),
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%);
  background-size: auto, 10px 10px, 10px 10px;
  background-position: 0 0, 0 0, 5px 5px;
  box-shadow: inset 0 0 0 1px var(--border);
}

.cp-ramp-chips {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* A chip sitting above the ramp, pointing at the position it marks. Above
   rather than on it, so the colour underneath stays visible — a handle drawn
   over the gradient hides the exact colour being adjusted. */
.cp-chip {
  position: absolute;
  pointer-events: auto;
  top: -10px;
  left: var(--at, 0%);
  width: 14px;
  height: 14px;
  margin-left: -7px;
  padding: 0;
  border: 2px solid var(--panel);
  border-radius: 4px;
  background: var(--stop-color, #fff);
  box-shadow: 0 0 0 1px var(--swatch-ring);
  cursor: grab;
  touch-action: none;
}

.cp-chip:active {
  cursor: grabbing;
}

.cp-chip.is-selected {
  /* The selected chip is taller rather than a different colour: it already
     carries a colour, and recolouring the marker would be a lie about the
     stop. */
  top: -14px;
  height: 18px;
  box-shadow: 0 0 0 1px var(--accent), 0 1px 4px rgba(0, 0, 0, 0.3);
}

.cp-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.cp-stops-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-weight: 600;
  color: var(--text-muted);
}

.cp-stops {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 132px;
  overflow-y: auto;
}

.cp-stop {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 2px;
  border-radius: 5px;
}

.cp-stop.is-selected {
  background: var(--accent-soft);
}

.cp-stop-position,
.cp-stop-opacity {
  flex: 0 0 40px;
  min-width: 0;
  height: 24px;
  padding: 0 3px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: center;
}

.cp-stop-hex {
  flex: 1 1 auto;
  min-width: 0;
  height: 24px;
  padding: 0 5px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-transform: uppercase;
}

.cp-stop-swatch {
  flex: 0 0 20px;
  min-width: 0;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  position: relative;
  overflow: hidden;
  background-color: var(--checker-a);
  background-image:
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%),
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
  box-shadow: inset 0 0 0 1px var(--border);
}

.cp-stop-swatch::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--swatch-color, #000);
  opacity: var(--swatch-alpha, 1);
}

/* ------------------------------------------------------------- library tab */

.cp-library {
  max-height: 320px;
  overflow-y: auto;
  gap: 1px;
}

.cp-library-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: left;
  cursor: pointer;
}

.cp-library-row:hover {
  background: var(--hover);
}

.cp-library-row.is-bound {
  background: var(--accent-soft);
}

.cp-library-swatch {
  flex: 0 0 18px;
  min-width: 0;
  width: 18px;
  height: 18px;
  border-radius: 4px;
  background: var(--swatch-color, #000);
  box-shadow: inset 0 0 0 1px var(--swatch-ring);
}

.cp-library-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

.cp-library-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cp-library-origin {
  color: var(--text-muted);
  font-size: 10px;
}

/* ------------------------------------------------------- token gear + list

   The gear lives inside the field's box, so a bound property reads as one
   control with a state rather than as a number that happens to have a button
   near it. */

.input-with-gear {
  display: flex;
  align-items: center;
  gap: 2px;
}

.input-with-gear input {
  flex: 1 1 auto;
  min-width: 0;
}

/* ---------------------------------------------------------------------------
   The number field: a glyph you can drag, the value, and a gear.

   One box with a border, three things inside it that have no borders of their
   own — which is what makes it read as a single control rather than as a row
   of three. Figma's inspector is built the same way, and the reason is that
   the glyph and the gear are *about* the number; drawn as separate controls
   they look like neighbours instead.
   --------------------------------------------------------------------------- */
.number-input {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  height: 26px;
  padding: 0 2px 0 0;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  background: var(--field);
}

.number-input:hover {
  background: var(--field-hover);
  border-color: var(--border);
}

.number-input:focus-within {
  border-color: var(--accent);
}

/* The input loses its own box: the wrapper is the box now. Leaving both gives
   a border inside a border, which at 26px tall looks like a rendering fault. */
.field .number-input input {
  height: 24px;
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 0 2px;
}

.field .number-input input:focus {
  outline: none;
  border: 0;
}

.scrub-handle {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 24px;
  padding: 0 3px;
  font-size: 10px;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--text-subtle);
  cursor: ew-resize;
  /* Without this a horizontal drag on a touch screen scrolls the panel and the
     scrub never receives a single move event. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

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

/* Only the gear on the row under the pointer, or the one doing something.
   Thirty gears down a panel is noise, and noise is where a bound property
   stops being visible. */
.number-input .token-gear {
  opacity: 0;
}

.number-input:hover .token-gear,
.number-input:focus-within .token-gear,
.number-input .token-gear:focus-visible,
.number-input .token-gear[aria-expanded='true'],
.number-input .token-gear.is-bound {
  opacity: 1;
}

/* A field whose label is its glyph has no caption to leave room for. */
.field.glyph-only > .field-label {
  display: none;
}

.token-gear {
  flex: 0 0 20px;
  min-width: 0;
  width: 20px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.55;
}

.token-gear:hover,
.token-gear[aria-expanded='true'] {
  opacity: 1;
  background: var(--hover);
  color: var(--text);
}

/* The gap field, once it has a gear: the box is the control and the input
   inside it loses its own border, exactly as a number field's does. Without
   this the input keeps its 26px box and its border inside the wrapper's, which
   at that height reads as a rendering fault. */
.number-input.gap-input > .bare-number {
  flex: 1 1 auto;
  height: 24px;
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 0 5px;
}

.number-input.gap-input > .bare-number:focus {
  outline: none;
}

.layout-gap .token-gear {
  flex: 0 0 22px;
  min-width: 0;
}

/* ---------------------------------------------------------------------------
   A bound field.

   The number and its drag handle are gone; the token is the field. Tinted with
   the accent rather than outlined, so that scanning a panel answers "what here
   is driven by the design system" in one pass, without reading a single label.

   The box keeps the number field's height and radius exactly. A bound field two
   pixels taller than the one beside it makes the panel look like it is coming
   apart, and this is a state half the rows can be in at once.
   --------------------------------------------------------------------------- */
.number-input.is-bound {
  border-color: transparent;
  background: var(--accent-soft);
}

/* The glyph a bound W or H keeps: still the label, no longer a handle. */
.number-input.is-bound .field-glyph {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 24px;
  padding: 0 3px;
  font-size: 10px;
  line-height: 1;
  color: var(--accent);
  user-select: none;
  -webkit-user-select: none;
}

/* Two controls in the slot a row reserved for one: the token gear beside the
   settings button on a stroke. */
.trailing-pair {
  display: flex;
  align-items: center;
  gap: 2px;
}

/* A selection that does not agree. Same shape as a bound chip, in the muted ink
   the panel already uses for "not one answer" — so it cannot be read as a token
   that happens to be called Mixed. */
.token-chip.is-mixed .token-chip-name,
.token-chip.is-mixed .token-chip-detach {
  color: var(--text-muted);
}

.token-chip {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  gap: 2px;
}

/* The name is a button because it does something: it re-opens the picker with
   this token marked, which is how you swap one token for another without
   detaching first. */
.token-chip-name {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1 1 auto;
  min-width: 0;
  height: 24px;
  padding: 0 2px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: left;
  cursor: pointer;
}

.token-chip-name:hover,
.token-chip-name[aria-expanded='true'] {
  background: var(--hover);
}

/* One line, clipped at the end rather than wrapped: a two-line chip would make
   the row taller than every other row in the panel. */
.token-chip-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Detach is always visible on a bound field — unlike the gear, which hides
   until the row is hovered. It is the way out of a state the field is *in*,
   and a way out you have to discover by hovering is not one. */
.token-chip-detach {
  flex: 0 0 20px;
  min-width: 0;
  width: 20px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--accent);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.75;
}

.token-chip-detach:hover {
  opacity: 1;
  background: var(--hover);
}

/* In the gap row the chip stands alone, with no number-input box around it, so
   it carries the tint itself. */
.layout-gap > .token-chip {
  height: 26px;
  padding: 0 2px 0 6px;
  border-radius: 5px;
  background: var(--accent-soft);
}

.token-popover {
  position: fixed;
  z-index: 61;
  width: 252px;
  max-height: 360px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.token-popover-head {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-weight: 600;
  color: var(--text-muted);
}

.token-popover .token-search {
  height: 26px;
  padding: 0 7px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-weight: 400;
}

.token-popover .token-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.token-popover .token-group {
  padding: 6px 6px 2px;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.token-popover .token-row {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: left;
  cursor: pointer;
}

.token-popover .token-row:hover {
  background: var(--hover);
}

.token-popover .token-row.is-bound {
  background: var(--accent-soft);
}

.token-popover .token-row.is-deprecated .token-name {
  text-decoration: line-through;
  opacity: 0.7;
}

.token-popover .token-swatch {
  flex: 0 0 14px;
  min-width: 0;
  width: 14px;
  height: 14px;
  border-radius: 3px;
  background: var(--swatch-color, #000);
  box-shadow: inset 0 0 0 1px var(--swatch-ring);
}

.token-popover .token-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.token-popover .token-value {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-family: var(--mono);
  font-size: 10px;
}

.token-detach {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: left;
  cursor: pointer;
}

.token-detach:hover {
  background: var(--hover);
  color: var(--text);
}

.token-detach[hidden] {
  display: none;
}

.token-popover .token-empty {
  margin: 8px 6px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

/* The sidebar is 264px and these rows carry four controls each, so every width
   here is deliberate: nothing grows, because there is nothing that wants to —
   four small numbers in a narrow column read better aligned than stretched, and
   a single flexible field would push the rest off the edge. */
/* The hex must stay readable: a field showing `#3737` is worse than useless,
   because it looks like a value rather than like a truncation. It gets the
   flexible width and a floor; everything else on the row is fixed. */
.color-row .hex-input {
  flex: 1 1 70px;
  min-width: 62px;
}

.color-row .alpha-input {
  flex: 0 0 52px;
}

/* Every numeric field in the editor is `<input type="text">`, so there are no
   spinner arrows to hide any more — the three blocks that hid them are gone.
   The reason for the change was arithmetic: `<input type="number">` refuses to
   hold `800+24`, and the browser hands back an empty string rather than the sum
   somebody typed. `number-input.ts` carries the rest of the reasoning. What the
   spinners were costing was width — Chromium reserves about fourteen pixels for
   them inside every box — which is why nothing here needs to be replaced. */

.color-row.is-compact {
  gap: 4px;
}

.color-row.is-compact .alpha-input {
  flex: 0 0 40px;
}

/* ------------------------------------------------------------ context menu

   On <body>, like the colour picker, and for the same reason: the panels clip
   and scroll their overflow, so a menu parented inside one is cut off at the
   panel edge. Above the picker in z-order — a menu opened over a popover has to
   be the thing you are looking at. */
.context-menu {
  position: fixed;
  /* Above every surface a menu can be opened from, and the file browser is the
     highest of them at 90. It used to be 70: the browser's own context menus —
     on a card, on a project, on the account row — were rendered *behind* the
     browser, so they were invisible and every click landed on the rail behind
     them. Nothing could catch it; the harness has no stacking contexts, and the
     menu was in the DOM with the right rows in it the whole time. */
  z-index: 130;
  min-width: 208px;
  max-width: 320px;
  padding: 5px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  /* A menu taller than the viewport scrolls rather than running off it; the
     placement can flip it but cannot make the screen bigger. */
  max-height: calc(100vh - 24px);
  overflow-y: auto;
  outline: none;
}

.context-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  height: 26px;
  padding: 0 8px 0 4px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  text-align: left;
  cursor: default;
}

/* Hover and keyboard highlight are the same state deliberately: two different
   highlights on screen at once is a menu you have to think about. */
.context-menu-item:hover:not(:disabled),
.context-menu-item.is-active:not(:disabled) {
  background: var(--accent-fill);
  color: var(--on-accent);
}

.context-menu-item:disabled {
  color: var(--text-subtle);
  cursor: default;
}

/* A fixed-width column for the tick, so labels line up whether or not the menu
   contains any toggles — the alternative is rows that shift sideways as the
   state changes. */
.context-menu-tick {
  flex: 0 0 12px;
  min-width: 0;
  font-size: 10px;
  line-height: 1;
  text-align: center;
}

.context-menu-label {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.context-menu-hint {
  flex: 0 0 auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-variant-numeric: tabular-nums;
}

/* The shortcut on a highlighted row. Mixed with the accent rather than a fixed
   white at 75%: the ink on the accent is white in the light theme and
   near-black in the dark one, so a fixed white is a hint that vanishes in one
   of them. */
.context-menu-item:hover:not(:disabled) .context-menu-hint,
.context-menu-item.is-active:not(:disabled) .context-menu-hint {
  color: color-mix(in srgb, var(--on-accent) 75%, var(--accent));
}

.context-menu-separator {
  height: 1px;
  margin: 5px 4px;
  background: var(--border);
}

/* ------------------------------------------------------------ colour picker

   Positioned on <body> rather than inside the inspector: the inspector clips
   its overflow and scrolls, so a popover parented there is cut off at the panel
   edge and scrolls away from its own swatch. */
.color-popover {
  position: fixed;
  z-index: 60;
  width: 232px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The saturation/value square: hue underneath, white left-to-right, black
   bottom-to-top. Two gradients over a flat colour is the whole thing — no
   canvas, no image, and sharp at any pixel density. */
.cp-area {
  position: relative;
  height: 148px;
  border-radius: 6px;
  cursor: crosshair;
  touch-action: none;
  background-color: var(--cp-hue-color, #f00);
  background-image:
    linear-gradient(to top, #000, transparent),
    linear-gradient(to right, #fff, transparent);
}

.cp-sliders {
  display: flex;
  align-items: center;
  gap: 8px;
}

.cp-tracks {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cp-track {
  position: relative;
  height: 10px;
  border-radius: 5px;
  cursor: pointer;
  touch-action: none;
}

.cp-hue {
  background: linear-gradient(
    to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%
  );
}

.cp-alpha {
  background-color: var(--checker-a);
  background-image:
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%),
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
}

.cp-alpha-fill {
  position: absolute;
  inset: 0;
  border-radius: 5px;
  pointer-events: none;
}

/* One handle class for all three controls. The white ring plus the dark outer
   ring is what keeps it visible over both ends of every gradient it sits on —
   a plain white dot disappears in the top-left corner of the square. */
.cp-handle {
  position: absolute;
  left: 0;
  top: 50%;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}

.cp-preview {
  width: 26px;
  height: 26px;
  flex: 0 0 26px;
  min-width: 0;
  border-radius: 50%;
  position: relative;
  overflow: hidden;
  background-color: var(--checker-a);
  background-image:
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%),
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
  box-shadow: inset 0 0 0 1px var(--border);
}

.cp-preview::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--cp-preview-color, #000);
  opacity: var(--cp-preview-alpha, 1);
}

.cp-dropper {
  flex: 0 0 26px;
  min-width: 0;
}

.cp-fields {
  display: flex;
  align-items: center;
  gap: 6px;
}

.cp-group {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  gap: 4px;
}

.cp-group[hidden] {
  display: none;
}

.cp-mode,
.cp-hex,
.cp-channel,
.cp-opacity {
  height: 26px;
  min-width: 0;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-family: var(--mono);
}

.cp-mode {
  flex: 0 0 58px;
  min-width: 0;
  font-family: inherit;
}

.cp-hex {
  flex: 1 1 auto;
  text-transform: uppercase;
}

.cp-channel {
  flex: 1 1 0;
  width: 0;
  text-align: center;
}

.cp-opacity {
  flex: 0 0 44px;
  min-width: 0;
  text-align: center;
}

.cp-area:focus-visible,
.cp-track:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.hex-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 26px;
  padding: 0 7px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.alpha-input {
  flex: 0 0 46px;
  /* `min-width: auto` on a flex item resolves to its *min-content* size, and
     for an `<input>` that is the width of its `size` attribute — twenty
     characters, about 155px. That silently beats `flex-basis`, so every number
     field in a row came out three times its declared width and pushed the rest
     of the row off the panel. The declared basis is the whole point of these
     rows; this is what makes it apply. */
  min-width: 0;
  height: 26px;
  padding: 0 4px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-family: inherit;
}

/* Alignment: segmented groups of icon buttons.

   The groups are separated by a gap rather than a divider line, and each group
   is a single rounded shape, so "which of these three is the same kind of
   thing" is answered by shape before it is answered by reading. */
.align-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

.align-group {
  display: flex;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  overflow: hidden;
  background: var(--field);
}

.align-group .icon-button {
  width: 32px;
  height: 28px;
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* The seam between buttons inside a group, drawn once so it cannot double up
   on the group's own border. */
.align-group .icon-button + .icon-button {
  box-shadow: inset 1px 0 0 var(--border);
}

.align-group .icon-button:hover {
  background: var(--border);
  color: var(--text);
}

.align-group .icon-button:active {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

/* A field with a small control pinned to its right — the aspect-ratio lock and
   the independent-corners toggle. The control changes what the neighbouring
   fields *mean*, so it sits beside them rather than on its own row. */
.field-with-trailing {
  display: flex;
  align-items: center;
  gap: 6px;
}

.field-with-trailing-main {
  flex: 1 1 auto;
  min-width: 0;
}

.field-with-trailing-group {
  display: flex;
  gap: 2px;
  flex: 0 0 auto;
}

.field-with-trailing .icon-button.is-on,
.field-with-trailing-group .icon-button.is-on {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

/* The inspector while points are selected. The title is a label rather than an
   input, because a point has no name to rename — and the count beside it is
   what says the fields below read across more than one of them. */
.inspector-points-name {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
}

.inspector-points-count {
  flex: 0 0 auto;
  padding-right: 4px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

/* Handle mirroring, as a segmented control like the alignment row above it.
   Drawn rather than named: the three states are arrangements of a point and its
   two handles, no icon set has them, and the text glyphs that stood in for them
   (`⋮ ∠ ↔`) were pictures of nothing — the row could only be read by hovering
   each button in turn. `mirror-control.ts` builds the SVG; it inherits
   `currentColor`, so the pressed state below is the only place a colour is
   named. */
.point-mirror {
  width: 44px;
  height: 28px;
  line-height: 1;
}

.point-mirror svg,
.vector-mirror-button svg {
  display: block;
  margin: 0 auto;
}

.align-group .point-mirror.is-on {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

.effect-header {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}

.effect-header .field {
  flex: 1 1 auto;
  min-width: 0;
}

/* A hidden paint stays legible — it is still the colour you will bring back —
   but reads as switched off rather than as merely pale. */
.color-row.is-hidden-paint .color-swatch,
.color-row.is-hidden-paint .hex-input,
.color-row.is-hidden-paint .alpha-input {
  opacity: 0.45;
}

.button-row {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin-top: 4px;
}

.button-row button,
.text-button {
  height: 26px;
  padding: 0 8px;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  background: var(--field);
  color: var(--text);
  font-family: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.button-row button:hover,
.text-button:hover {
  background: var(--bg-sunken);
  border-color: var(--border-strong);
}

.text-button {
  width: 100%;
  color: var(--accent);
  border-style: dashed;
}

/* A full-width action inside a section — "Crop the picture".

   Solid rather than the dashed `.text-button` beside it, and the difference is
   meant: dashed-and-accent is this interface's shape for *adding* something
   that is not there yet, and entering a mode is not that. Full width because
   it is the section's verb, not one of a row of properties. */
.inspector-wide-button {
  width: 100%;
  height: 28px;
  padding: 0 8px;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  background: var(--field);
  color: var(--text);
  font-family: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.inspector-wide-button:hover {
  background: var(--bg-sunken);
  border-color: var(--border-strong);
}

/* ----------------------------------------------------------- empty states */

.empty-state {
  padding: 24px 16px;
  text-align: center;
}

/* The actions under an empty state, as one column of equal width. Inline
   buttons in a centred box size themselves to their own labels, so a two-word
   action and a five-word one came out as two different shapes with nothing
   between them — reported by the owner with a screenshot of the Tokens
   panel. */
.empty-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
}

.empty-actions .labelled-button {
  justify-content: center;
  gap: 8px;
  text-align: center;
}

/* The label as its own flex item, so a long one wraps inside a box of its own
   rather than as an anonymous item stretched to the button's line. */
.button-label {
  min-width: 0;
}

.empty-title {
  margin: 0 0 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
}

.empty-hint {
  margin: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
  color: var(--text-subtle);
}

/* ------------------------------------------------------------ status bar */

.status-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  height: var(--status-height);
  padding: 0 12px;
  background: var(--panel);
  border-top: 1px solid var(--border);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
  flex: 0 0 auto;
}

.status-message[data-tone='error'] {
  color: var(--danger);
}

.status-message[data-tone='success'] {
  color: var(--positive);
}

/* The build number: present always, quiet always. It is read once, when
   something is being reported, and must not compete with the frame time. */
.status-build {
  opacity: 0.7;
}

.status-metric {
  font-variant-numeric: tabular-nums;
  color: var(--text-subtle);
}

.status-metric.warn {
  color: var(--danger);
}

/* -------------------------------------------------------- command palette */

.palette-overlay {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: 14vh;
  z-index: 100;
}

.palette-overlay[hidden] {
  display: none;
}

.palette {
  width: min(560px, 92vw);
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.palette-input {
  width: 100%;
  height: 46px;
  padding: 0 14px;
  border: none;
  border-bottom: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 15px;
}

.palette-input:focus {
  outline: none;
}

.palette-results {
  max-height: 46vh;
  overflow-y: auto;
  padding: 4px;
}

.palette-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border-radius: 6px;
  cursor: pointer;
}

.palette-row.active {
  background: var(--accent-soft);
}

.palette-row.disabled {
  opacity: 0.38;
  cursor: default;
}

.palette-title {
  flex: 1 1 auto;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

.palette-section {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-subtle);
}

.palette-shortcut {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--text-subtle);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
}

.palette-empty {
  padding: 20px;
  text-align: center;
  font-size: 12px;
  color: var(--text-subtle);
}

/* ------------------------------------------------------------ boot states */

.booting {
  flex: 1 1 auto;
  display: grid;
  place-content: center;
  gap: 10px;
  text-align: center;
  color: var(--text-muted);
}

/*
 * The boot line, in the language the head snippet decided.
 *
 * Two paragraphs in the markup and one of them hidden here, because this text
 * is painted before any module runs: there is nothing to look it up in yet.
 * Keyed off `<html lang>` rather than a class of our own, since that attribute
 * has to be right anyway — a screen reader picks its voice from it.
 *
 * The default direction hides Russian: an unknown or missing `lang` leaves the
 * English line, which is the same fallback `i18n.ts` uses.
 */
.boot-lang[data-lang='ru'] {
  display: none;
}

:root[lang='ru'] .boot-lang[data-lang='ru'] {
  display: block;
}

:root[lang='ru'] .boot-lang[data-lang='en'] {
  display: none;
}

.booting-mark {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.5px;
  color: var(--text);
}

.boot-error {
  max-width: 520px;
  margin: 12vh auto;
  padding: 24px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
}

.boot-error h1 {
  margin: 0 0 8px;
  font-size: 17px;
}

.boot-error p {
  margin: 0 0 8px;
  color: var(--text-muted);
  line-height: 1.55;
}

.boot-hint {
  font-size: 12px;
  color: var(--text-subtle);
}

.boot-error button {
  margin-top: 8px;
  height: 30px;
  padding: 0 14px;
  border: none;
  border-radius: 6px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-family: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

/* ------------------------------------------------------- system respect */

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

@media (forced-colors: active) {
  .tool-button.active,
  .palette-row.active,
  .layer-row.selected {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }
}

/* ------------------------------------------------------- lint findings */

.finding {
  padding: 7px 9px;
  margin-bottom: 6px;
  border-left: 3px solid var(--border-strong);
  border-radius: 0 5px 5px 0;
  background: var(--bg-sunken);
}

.finding-error {
  border-left-color: var(--danger);
}

.finding-warning {
  border-left-color: var(--warning);
}

.finding-info {
  border-left-color: var(--text-subtle);
}

.finding-message {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.45;
  color: var(--text);
}

.finding-fix {
  margin-top: 3px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.45;
  color: var(--text-muted);
}

/* ---------------------------------------------------------------------------
   Icons — Phosphor (https://phosphoricons.com), self-hosted.

   Two faces, two files, both served by this origin: `font-src 'self'` covers
   them and no request leaves the page for an icon. The files live in
   `apps/web/fonts/` and `tools/build.mjs` copies them into `dist/`; the names
   here, the families in `icons.ts` and the files on disk are held against each
   other by `stylesheet.test.ts`, because a `src` naming a file nobody ships is
   a 404 in a console nobody has open and a toolbar of letters on screen.

   `font-display: block` rather than `swap`: the fallback is a *letter*, not a
   different cut of the same picture, so swapping one in and out is a visible
   flicker of a completely different control. The gate in `icons.ts` decides
   when the letters go, and it decides once.

   The font is an enhancement, not a dependency. Until the root carries
   `icons-ready` the text fallback is what renders, so a font file that will not
   load degrades to distinct letters rather than to a row of identical tofu
   boxes.
   --------------------------------------------------------------------------- */

@font-face {
  font-family: 'Phosphor';
  src: url('./fonts/Phosphor.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: block;
}

@font-face {
  font-family: 'Phosphor-Fill';
  src: url('./fonts/Phosphor-Fill.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: block;
}

/* Icon size is set once, here, and every button sizes itself around it.

   Outline glyphs need more room than filled ones to read: a filled shape is
   legible from its silhouette, an outline from its interior detail, and that
   detail is the first thing to go as the glyph shrinks.

   **In px, not rem.** `html` here sets `font-size: 13px`, so `1rem` is 13px and
   not the 16px everyone reads it as — the old `1.05rem` looked like 17px in the
   source and rendered at 13.65px. An icon's box is its font-size (`.icon` is
   1em square), so this number is the one thing in the file that must be
   literal. */
.icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1em;
  height: 1em;
  font-size: 18px;
  line-height: 1;
  flex: none;
}

/* `--icon-ink` is read here and declared at the top of the file, with the rest
   of the tokens. A container that wants fainter icons sets it on itself and
   every icon inside it follows. */

/* The glyph is one private-use character, so nothing here is about ligatures:
   `letter-spacing: normal` because a panel that tightened its text would
   otherwise shift the picture off centre, and `font-size: inherit` because
   `.icon` is a 1em box and the glyph has to fill it exactly. */
.icon .icon-glyph {
  display: none;
  font-family: 'Phosphor';
  font-weight: normal;
  font-style: normal;
  font-size: inherit;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  opacity: var(--icon-ink, 1);
  -webkit-font-smoothing: antialiased;
}

.icon-fallback {
  font-size: 0.78em;
  font-weight: 600;
  letter-spacing: -0.02em;
}

.icons-ready .icon .icon-glyph {
  display: inline-block;
}

.icons-ready .icon-fallback {
  display: none;
}

/* An icon whose name this app has no symbol for keeps its letter, font or no
   font. The glyph element would be empty — the font renders nothing for an
   empty ligature — and with the fallback hidden behind it the control would be
   blank, which is the one state the fallbacks exist to prevent. It would also
   appear only when everything else was working, which is the worst time to
   discover it. `icons.ts` sets `data-symbol` for exactly this rule. */
.icons-ready .icon[data-symbol='none'] .icon-glyph {
  display: none;
}

.icons-ready .icon[data-symbol='none'] .icon-fallback {
  display: inline;
}

/* Filled icons: a second family, because a Phosphor weight is its own file.
   The layer tree draws a main component solid and an instance hollow, and that
   distinction costs a second download — which is why `icons.ts` waits for both
   faces before setting `icons-ready`, rather than opening the gate on the
   outline one and showing tofu where the filled icons belong. */
.icon[data-weight='fill'] .icon-glyph {
  font-family: 'Phosphor-Fill';
}

/* There used to be two rules for this class, seven hundred lines apart, one
   asking for 18px and one for a 26px minimum. The minimum won, so the 18px was
   dead code that nonetheless read as the real size to anyone changing it — the
   kind of quiet disagreement that makes a later "why is this not 18px?" take an
   afternoon. One rule now. */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  min-height: 26px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.icon-button:hover {
  background: var(--bg-sunken);
  color: var(--text);
}

.labelled-button .icon {
  margin-right: 6px;
}

/* ---------------------------------------------------------------------------
   Document bar — the top strip, now about the document rather than drawing.
   --------------------------------------------------------------------------- */

/* A button, not a div: it is the way back to the file browser, which is where
   Figma puts it and where anyone who has used a design tool looks for it. */
.doc-crumb {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 3px 6px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

/* The two halves light up **separately**, and the row itself does not.
   They are two targets with two meanings — go to the folder, rename the file —
   and one highlight across both says they are one control, which is exactly
   the confusion that split them into two elements in the first place. */
.doc-crumb .crumb-parent {
  padding: 3px 6px;
  border: 0;
  border-radius: 5px;
  /* A `<button>` with no background declared is a grey pill: the browser's own
     default, which is what put a filled chip around `Drafts` while every other
     control in this bar was flat. */
  background: none;
  color: var(--text-subtle);
  font: inherit;
  font-size: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}

.doc-crumb .crumb-parent:hover {
  background: var(--hover);
  color: var(--text);
}

.doc-crumb .crumb-sep {
  color: var(--text-subtle);
}

.doc-crumb .crumb-name {
  padding: 3px 6px;
  border-radius: 5px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 40ch;
  cursor: text;
}

.doc-crumb .crumb-name:hover {
  background: var(--hover);
}

/* ---------------------------------------------------------------------------
   Pages
   --------------------------------------------------------------------------- */

/* Height comes from the splitter, falling back to the old viewport-relative
   slice when nothing has been dragged yet. `position: relative` because the
   handle is positioned against this element. */
/* The wrapper carries the height and the resize handle; the panel inside it
   scrolls. Splitting the two is what keeps the handle alive across the panel's
   `replaceChildren`. */
.pages-section {
  position: relative;
  height: var(--pages-height, 26vh);
  flex: 0 0 var(--pages-height, 26vh);
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* No divider line between the pages list and the layer tree.
 *
 * The splitter draws a hairline at rest (`::before`) purely as a visible seam,
 * so the two regions read as two panels; the owner asked for that separator to
 * go. Only the line is removed, and only on the pages splitter (the handle is a
 * child of `.pages-section`): the drag target and its accent band on hover
 * (`::after`) are untouched, so the list can still be resized — the line was
 * never the thing you grab. Other splitters keep their hairline. */
.pages-section > .splitter-vertical::before {
  display: none;
}

.pages-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 4px 6px 6px;
}

.page-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.page-row {
  display: flex;
  align-items: center;
  gap: 2px;
  border-radius: var(--radius);
  padding-right: 2px;
}

.page-row:hover {
  background: var(--bg-sunken);
}

.page-row.current {
  background: var(--accent-soft);
}

/**
 * A page called `---` is a rule between pages.
 *
 * The row keeps its height so the list does not jump when a page is renamed
 * into one and back, and the line is a child rather than a border on the row
 * itself: a border would be painted at the row's full width, and this is inset
 * so it reads as a divider inside the list rather than as the edge of a panel.
 */
.page-row.is-separator {
  min-height: 17px;
  padding: 0 8px;
  cursor: default;
}

.page-row.is-separator:hover {
  background: transparent;
}

.page-separator-rule {
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* The one you are on, which can happen: a page is renamed while it is open. */
.page-row.is-separator.current {
  background: transparent;
}

.page-row.is-separator.current .page-separator-rule {
  background: var(--accent);
}

/* What the list says when a search matches nothing — an empty list and a
   filter that matches nothing look identical, and only one of them is a state
   somebody typed themselves. */
.page-empty {
  margin: 0;
  padding: 8px 10px;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.page-open {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  /* Comfortably hittable: this is a row you also rename by double-clicking. */
  min-height: 30px;
  padding: 0 6px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.page-row.current .page-open {
  font-weight: 600;
}

.page-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.page-count {
  font-variant-numeric: tabular-nums;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-subtle);
}

.page-name-field {
  flex: 1;
  min-width: 0;
  min-height: 30px;
  padding: 0 6px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.page-add {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 30px;
  margin-top: 4px;
  padding: 0 6px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.page-add:hover {
  background: var(--bg-sunken);
  color: var(--text);
}

/* ---------------------------------------------------------------------------
   Sidebar tabs — Layers and Tokens are two views of one file.
   --------------------------------------------------------------------------- */

/* Scrolls rather than overflowing. With four tabs `flex: 1` fitted; the fifth
   pushed the strip past the sidebar's edge, and the part that stuck out sat
   *under the canvas*, where it was visible and could not be clicked — the same
   `min-width: auto` flexbox trap that is documented further down, arriving as a
   tab that appeared broken rather than as a squashed one. */
/* ------------------------------------------------------------- panel rail

  The five panels of the left sidebar, as a vertical rail of icons. They were a
  horizontal strip of text tabs and five did not fit: the fifth was off the edge
  at the default sidebar width, so the comments panel was reachable only by
  keyboard. A rail costs a fixed width and does not care how many entries it
  holds. */

.panel-rail {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  gap: 2px;
  width: 72px;
  padding: 8px 6px;
  background: var(--panel);
  border-right: 1px solid var(--border);
  overflow-y: auto;
  scrollbar-width: none;
}

.panel-rail::-webkit-scrollbar {
  display: none;
}

.panel-rail-item {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-width: 0;
  padding: 8px 2px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  cursor: pointer;
}

.panel-rail-item:hover {
  background: var(--bg-sunken);
  color: var(--text);
}

.panel-rail-item.active {
  background: var(--bg-sunken);
  /* The label under this icon is 9px. `--accent` on `--bg-sunken` is 3.02:1,
     which is a word nobody can read at that size — bug 466's other half. */
  color: var(--on-accent-soft);
}

.panel-rail-label {
  overflow: hidden;
  max-width: 100%;
  font-size: var(--type-body-small);
  letter-spacing: 0.2px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   Tokens panel
   --------------------------------------------------------------------------- */

.tokens-body {
  padding: 8px 6px;
}

.token-section {
  margin-bottom: 10px;
}

/* ---------------------------------------------------------------------------
   Tokens a file names and does not have.

   Absent unless there are some — the panel's other sections work the same way.
   It reads as a warning rather than as a list, because every row is a claim the
   file cannot keep.
   --------------------------------------------------------------------------- */
.token-missing {
  margin: 8px 10px 12px;
  padding: 10px;
  border: 1px solid var(--danger);
  border-radius: 8px;
  background: var(--danger-soft);
  color: var(--text);
}

.token-missing .empty-title,
.token-missing .empty-hint {
  color: var(--text);
}

.token-missing-row {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 26px;
}

.token-missing-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--type-body-small);
  letter-spacing: var(--type-body-small-tracking);
}

.token-missing-uses {
  flex: none;
  color: var(--text-muted);
  font-size: var(--type-body-small);
  letter-spacing: var(--type-body-small-tracking);
}

.token-heading {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 0 4px 4px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-subtle);
}

/* How many tokens the list below holds. Beside the word rather than in a hint
   on the right, because it is part of the heading's sentence: "Tokens 61" is
   the answer to the question this panel is opened with, and the panel used to
   answer it with "No token sets loaded" — see `render` in `tokens-panel.ts`. */
.token-heading-count {
  margin-right: auto;
  font-weight: 400;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  color: var(--text-subtle);
}

.token-hint {
  margin-left: auto;
  font-size: 10px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  opacity: 0.8;
}

.theme-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.theme-chip {
  min-height: 26px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 13px;
  background: var(--panel);
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.theme-chip:hover {
  background: var(--bg-sunken);
  color: var(--text);
}

.theme-chip.active {
  background: var(--accent-fill);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

.set-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.set-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 28px;
  padding: 0 4px;
  border-radius: var(--radius);
  font-size: 12px;
}

.set-row:hover {
  background: var(--bg-sunken);
}

.set-row.state-disabled .set-name,
.set-row.state-disabled .set-count {
  opacity: 0.45;
}

.set-state {
  min-width: 34px;
  height: 18px;
  padding: 0 4px;
  border: 0;
  border-radius: 4px;
  font: inherit;
  font-size: var(--type-body-small);
  font-weight: 700;
  letter-spacing: 0.3px;
  cursor: pointer;
  flex: none;
}

.state-enabled .set-state {
  background: var(--accent-fill);
  color: var(--on-accent);
}

.state-source .set-state {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

.state-disabled .set-state {
  background: var(--bg-sunken);
  color: var(--text-subtle);
}

.set-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.set-count {
  font-variant-numeric: tabular-nums;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-subtle);
}

.set-actions {
  display: none;
  gap: 0;
}

.set-row:hover .set-actions,
.set-row:focus-within .set-actions {
  display: flex;
}

.token-search {
  width: 100%;
  min-height: 28px;
  margin-bottom: 8px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.token-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.token-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 28px;
  padding: 0 4px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.token-row:hover {
  background: var(--bg-sunken);
}

.token-swatch {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  border-radius: 4px;
  background: var(--bg-sunken);
  color: var(--text-subtle);
  font-size: 10px;
  flex: none;
}

.token-swatch.is-colour {
  /* The token's own colour, from the property every swatch here uses — see the
     note in `tokens-panel.ts`. */
  background: var(--swatch-color, var(--bg-sunken));
  /* A border, because a white swatch on a white panel is invisible. */
  box-shadow: inset 0 0 0 1px var(--swatch-ring);
}

.token-path {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.token-value {
  font-variant-numeric: tabular-nums;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-subtle);
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.token-value.is-broken {
  color: var(--danger);
}

.token-impact {
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--accent-soft);
  color: var(--on-accent-soft);
  font-size: 10px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
  flex: none;
}

/* How many layers wear this token — and the way to see them. A button rather
   than a badge, because it does something; muted rather than accent, so the
   count does not compete with the impact badge beside it, which answers a
   different question ("what other tokens depend on this"). */
.token-usage {
  flex: none;
  min-width: 18px;
  height: 16px;
  padding: 0 4px;
  border: 0;
  border-radius: 8px;
  background: var(--hover);
  color: var(--text-muted);
  font: inherit;
  font-size: 10px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
  cursor: pointer;
}

.token-usage:hover {
  color: var(--text);
}

.token-form-detach {
  flex: none;
  height: 28px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.token-empty {
  margin: 8px 4px;
  font-size: 12px;
  color: var(--text-subtle);
}

/* --- authoring a token ----------------------------------------------------

   The row's primary action is "apply this to the selection"; editing is a
   separate button beside it, because a row whose click sometimes applies and
   sometimes opens a form is a row nobody clicks confidently. */

.token-row-wrap {
  display: flex;
  align-items: center;
  gap: 2px;
}

.token-row-wrap .token-row {
  flex: 1 1 auto;
  min-width: 0;
}

.token-row-wrap .icon-button {
  flex: none;
  opacity: 0;
}

.token-row-wrap:hover .icon-button,
.token-row-wrap .icon-button:focus-visible {
  opacity: 1;
}

.token-form-popover {
  width: 240px;
  padding: 10px;
}

.token-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.token-form-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.token-form-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.token-form-input {
  height: 28px;
  min-width: 0;
  padding: 0 7px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.token-form-actions {
  display: flex;
  gap: 6px;
  margin-top: 2px;
}

.token-form-save {
  flex: 1 1 auto;
  min-width: 0;
  height: 28px;
  border: 0;
  border-radius: 5px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.token-form-delete {
  flex: none;
  height: 28px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: transparent;
  color: var(--danger);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

/* --- Anchored popovers ---------------------------------------------------

   Shared by the stroke settings, the blend-mode menu and the effect editors.
   `position: fixed` because the inspector scrolls and an absolutely-positioned
   popover scrolls away from the button that opened it. */
.popover {
  position: fixed;
  z-index: 60;
  width: 232px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.popover .field {
  margin: 0;
}

/* ---------------------------------------------------------------------------
   Image settings: the crop rectangle and the seven corrections.

   Wider than the standard popover because a row here is a label, a slider and a
   number — at 232px the slider is 60px wide, which is a control you cannot aim
   with. The sliders are the reason this is a panel rather than a fill row: seven
   of them in the fill list would make an image four times the height of every
   other paint.
   --------------------------------------------------------------------------- */
.popover.image-settings {
  width: 288px;
}

.image-settings-heading {
  margin: 4px 0 0;
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* The gear on an image fill row, lit when the image has actually been changed.
   A row of identical grey gears tells nobody which photograph has been adjusted,
   and "did I already fix that one" is the question this answers at a glance. */
.image-gear.is-on {
  color: var(--accent);
}

.adjust-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.adjust-slider {
  flex: 1 1 auto;
  min-width: 0;
  height: 18px;
  accent-color: var(--accent);
  /* A horizontal drag on a touch screen scrolls the panel otherwise — the same
     reason the scrub handle sets it. */
  touch-action: none;
}

.adjust-number {
  flex: 0 0 46px;
  width: 46px;
  /* A fixed flex-basis is a *suggestion* until `min-width` is reset: the
     default `min-width: auto` lets the input's own content size win, so a
     three-digit value would push the slider beside it out of the row. */
  min-width: 0;
  padding: 3px 6px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: right;
}

.adjust-number:focus {
  border-color: var(--accent);
  outline: none;
}

/* The aspect presets. They wrap rather than scroll: six short labels on two
   lines read as a set, and a horizontal scroller hides half of them behind a
   gesture nobody knows is there. */
.crop-ratios {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.crop-ratio {
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 10px;
  white-space: nowrap;
  cursor: pointer;
}

.crop-ratio:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.popover-title {
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* A menu-shaped popover: full-width rows, no field labels. */
.popover-menu {
  width: 200px;
  padding: 6px;
  gap: 0;
}

.popover-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-width: 0;
  padding: 5px 8px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.popover-menu button:hover,
.popover-menu button:focus-visible {
  background: var(--hover);
}

.popover-menu button[aria-checked='true'] {
  font-weight: 600;
}

/* The tick column is reserved on every row, so the labels line up whether or
   not a row is the selected one. Without it the list shifts sideways as the
   selection moves, which reads as the menu twitching. */
.popover-menu .menu-tick {
  width: 12px;
  flex: 0 0 12px;
  min-width: 0;
  text-align: center;
  color: var(--accent);
}

.popover-menu hr {
  height: 1px;
  margin: 4px 2px;
  border: 0;
  background: var(--border);
}

/* "Mixed" reads as a value, not as a button — it sits in the same slot as a
   number input and must not look like an action. It is clickable so the four
   real values are one press away. */
.mixed-value {
  flex: 1 1 0;
  min-width: 0;
  padding: 4px 7px;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  background: var(--field);
  color: var(--text-muted);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.mixed-value:hover {
  color: var(--text);
  border-color: var(--border-strong, var(--border));
}

/* --- Dragging layer rows -------------------------------------------------- */

.layer-row.is-dragging {
  opacity: 0.45;
}

/* The line sits *between* rows, so it needs to take no vertical space of its
   own — otherwise every row below the pointer jumps down by two pixels as the
   indicator moves, and the tree appears to shudder during the drag. */
.drop-line {
  height: 0;
  margin: 0;
  border-top: 2px solid var(--accent);
  pointer-events: none;
}

/* "Into this container" is a different answer from "next to it", and a line
   cannot express the difference. */
.layer-row.drop-into {
  box-shadow: inset 0 0 0 2px var(--accent);
  border-radius: 4px;
}

.layer-name-field {
  flex: 1 1 auto;
  min-width: 0;
  padding: 1px 4px;
  border: 1px solid var(--accent);
  border-radius: 4px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
}

/* --- Keyboard reference --------------------------------------------------- */

.shortcuts-sheet {
  position: fixed;
  z-index: 80;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(640px, 90vw);
  max-height: 78vh;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  overflow: auto;
}

.shortcuts-sheet h3 {
  margin: 0 0 12px;
  font-size: 14px;
}

/* Two columns, because the list is long and a single column turns the sheet
   into a scroll no one reads to the end of. */
.shortcuts-body {
  column-count: 2;
  column-gap: 24px;
}

.shortcut-section {
  margin: 12px 0 6px;
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  break-after: avoid;
}

.shortcut-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 0;
  break-inside: avoid;
}

/* A fixed key column so the descriptions line up: the eye scans down the keys,
   and a ragged left edge makes that scan impossible. */
.shortcut-keys {
  flex: 0 0 96px;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text);
  white-space: nowrap;
}

.shortcut-title {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-muted);
  font-size: 12px;
}

/* --- Assets --------------------------------------------------------------- */

.assets-body {
  padding: 8px;
}

.asset-search {
  width: 100%;
  min-width: 0;
  height: 26px;
  margin-bottom: 8px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.asset-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.asset-row {
  display: flex;
  align-items: center;
  gap: 4px;
  /* Indent comes from the folder depth the panel writes onto the row. A nested
     list of real elements would be tidier markup and worse behaviour: the rows
     have to stay one flat sequence for the search results, which cut across
     folders. */
  padding-left: calc(var(--asset-depth, 0) * 12px);
}

/* A folder in the library. It is a button, not a heading with a button inside:
   the whole strip toggles, and a 12px twisty is a target nobody hits. */
.asset-group {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 6px;
  padding-left: calc(6px + var(--asset-depth, 0) * 12px);
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-align: left;
  cursor: pointer;
}

.asset-group:hover,
.asset-group:focus-visible {
  background: var(--hover);
  color: var(--text);
}

.asset-group .icon {
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

.asset-group-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The whole row is the place-an-instance button, so the target is the width of
   the panel rather than a small icon. Placing is what this list is for. */
.asset-place {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 6px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.asset-place:hover,
.asset-place:focus-visible {
  background: var(--hover);
}

.asset-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Zero instances is what distinguishes a component in use from one left over,
   so the count is always shown rather than hidden when it is zero. */
.asset-count {
  flex: 0 0 auto;
  min-width: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The family name is the longest value in the section and gets the full width;
   everything else in Typography pairs off. */
.font-family {
  flex: 1 1 auto;
  min-width: 0;
  height: 26px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
}

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

.stacked-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  margin-bottom: 6px;
}

/* `.field-label` carries `flex: 0 0 62px` for the side-by-side case. In a
   column that basis is read as a *height*, so every label floated 62px above
   the control it names and the block grew to twice its size. */
.stacked-field > .field-label {
  flex: 0 0 auto;
  min-width: 0;
}

.icon-choice {
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  background: var(--field);
}

.icon-choice .icon-button {
  flex: 1 1 0;
  min-width: 0;
}

/* Alignment and gap sit side by side because they are adjusted together while
   looking at the result. */
.layout-align-row {
  display: flex;
  gap: 8px;
  align-items: flex-start;
}

.layout-align-row > .stacked-field {
  flex: 1 1 0;
  min-width: 0;
}

/* Nine dots, in a square, because the control is a picture of the frame. */
.align-pad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
  aspect-ratio: 1;
  width: 100%;
  padding: 4px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  background: var(--field);
}

.align-dot {
  position: relative;
  border: 0;
  border-radius: 4px;
  background: none;
  cursor: pointer;
}

/* The dot is drawn with a pseudo-element so the hit target stays the whole
   ninth of the pad — a 4px target is unusable, and nine of them in a row is
   the worst version of that. */
.align-dot::after {
  content: '';
  position: absolute;
  inset: 50%;
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: var(--text-muted);
  opacity: 0.5;
}

.align-dot:hover::after {
  opacity: 1;
}

.align-dot.is-on::after {
  width: 10px;
  height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: 2px;
  background: var(--accent);
  opacity: 1;
}

.align-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.layout-gap {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.bare-number {
  width: 100%;
  min-width: 0;
  height: 26px;
  padding: 0 7px;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  background: var(--field);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
}

.bare-number:hover {
  background: var(--field-hover);
  border-color: var(--border);
}

/* The paint-type strip at the top of the picker. */
.cp-kinds {
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 6px;
}

.cp-kind {
  flex: 1 1 0;
  min-width: 0;
  height: 24px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.cp-kind:hover {
  background: var(--hover);
  color: var(--text);
}

.cp-kind.is-on {
  background: var(--accent-fill);
  color: var(--on-accent);
}

/* --- Avatars ---------------------------------------------------------------
   One monogram, used by the account row, the sign-in screen and the file
   cards. The colour is an *index* stored on the account rather than a hex
   value, so the palette can change with the theme — a hex baked into the
   record at sign-up would still be the 2026 palette in the 2027 build, and
   would not adapt to a light theme at all. */

.avatar {
  --avatar-size: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  background: var(--avatar-bg, var(--identity-7));
  color: #fff;
  font-size: calc(var(--avatar-size) * 0.4);
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1;
  user-select: none;
}

.avatar[data-colour='0'] { --avatar-bg: var(--identity-0); }
.avatar[data-colour='1'] { --avatar-bg: var(--identity-1); }
.avatar[data-colour='2'] { --avatar-bg: var(--identity-2); }
.avatar[data-colour='3'] { --avatar-bg: var(--identity-3); }
.avatar[data-colour='4'] { --avatar-bg: var(--identity-4); }
.avatar[data-colour='5'] { --avatar-bg: var(--identity-5); }
.avatar[data-colour='6'] { --avatar-bg: var(--identity-6); }
.avatar[data-colour='7'] { --avatar-bg: var(--identity-7); }

/* --- Sign in, sign up, account settings ------------------------------------
   Over everything, including the file browser: it is the one screen where the
   rest of the application is not usable yet. */

.auth-screen {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--bg-sunken);
  overflow-y: auto;
}

/* Opened over a working editor rather than in front of a locked one, so the
   backdrop dims instead of replacing. */
.auth-screen.is-dialog {
  background: var(--overlay);
}

.auth-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-width: 380px;
  margin: auto;
  padding: 28px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.auth-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 700;
}

/*
 * The stage badge, beside the wordmark on the first screen.
 *
 * Deliberately quiet rather than a warning colour: this is a statement of fact
 * about the build, not an error, and a red badge on the sign-in screen reads as
 * "something is wrong right now" — which would be a lie every time it is true.
 */
.auth-stage {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid var(--accent);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* The wordmark is 700 and 15px; the badge must not compete with it. */
  line-height: 1.4;
  cursor: default;
}

.auth-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-size: 14px;
}

.auth-card h1 {
  margin: 8px 0 0;
  font-size: 20px;
}

.auth-sub {
  margin: 0 0 4px;
  color: var(--text-muted);
  font-size: 12px;
}

.auth-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.auth-label {
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-weight: 600;
}

.auth-field input,
.auth-profile input {
  width: 100%;
  min-width: 0;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

.auth-field input:focus-visible,
.auth-profile input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}

/* Four segments and a word. The word is what people read; the bars are what
   they notice. */
.auth-meter {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 6px;
}

.auth-meter-bar {
  flex: 1 1 auto;
  min-width: 0;
  height: 3px;
  border-radius: 2px;
  background: var(--border-strong);
}

.auth-meter-bar.is-on {
  background: var(--positive);
}

.auth-meter-label {
  flex: 0 0 auto;
  min-width: 44px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: right;
}

.auth-error {
  padding: 8px 10px;
  border: 1px solid var(--danger);
  border-radius: 6px;
  color: var(--danger);
  font-size: 12px;
}

.auth-error.is-ok {
  border-color: var(--positive);
  color: var(--positive);
}

.auth-submit {
  height: 36px;
  border: 0;
  border-radius: 8px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
  cursor: pointer;
}

.auth-submit:disabled {
  opacity: 0.6;
  cursor: default;
}

.auth-secondary {
  height: 32px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.auth-secondary.is-danger {
  border-color: var(--danger);
  color: var(--danger);
}

.auth-swap,
.auth-cancel {
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.auth-cancel {
  color: var(--text-muted);
}

.auth-known {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 4px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.auth-known-label {
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-weight: 600;
}

.auth-known-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.auth-known-row:hover {
  background: var(--hover);
}

.auth-known-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.auth-known-name {
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 500;
}

.auth-known-email {
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.auth-note {
  margin: 4px 0 0;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
}

/*
 * The way out of the local account list and into a real one.
 *
 * Boxed and set apart, because it is the answer to the question this screen
 * used to answer with a paragraph — "where is the sign-in with Yandex" — and an
 * answer that looks like the small print below it is an answer nobody finds.
 */
.auth-gateway {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-sunken);
}

.auth-gateway .auth-note {
  margin: 0;
}

.auth-workspace {
  display: inline-block;
  margin: 8px 0;
  color: var(--accent);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
  text-decoration: none;
}

.auth-workspace:hover {
  text-decoration: underline;
}

.auth-profile {
  max-width: 440px;
}

.auth-profile-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.auth-colours {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.auth-swatches {
  display: flex;
  gap: 6px;
}

.auth-swatch {
  width: 22px;
  height: 22px;
  border: 2px solid transparent;
  border-radius: 50%;
  cursor: pointer;
}

.auth-swatch.is-on {
  border-color: var(--text);
}

.auth-swatch[data-colour='0'] { background: var(--identity-0); }
.auth-swatch[data-colour='1'] { background: var(--identity-1); }
.auth-swatch[data-colour='2'] { background: var(--danger); }
.auth-swatch[data-colour='3'] { background: var(--identity-3); }
.auth-swatch[data-colour='4'] { background: var(--identity-4); }
.auth-swatch[data-colour='5'] { background: var(--identity-5); }
.auth-swatch[data-colour='6'] { background: var(--identity-6); }
.auth-swatch[data-colour='7'] { background: var(--identity-7); }

.auth-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.auth-section summary {
  color: var(--text-muted);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.auth-section.is-danger summary {
  color: var(--danger);
}

/* --- File browser --------------------------------------------------------- */

/* Full-screen rather than a panel. Choosing a file is a mode, not a detail of
   the file you are in — the canvas behind it is a distraction from the only
   decision on screen. */
.file-browser {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--bg);
}

.fb-shell {
  display: flex;
  height: 100%;
  min-height: 0;
}

.fb-rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 0 0 244px;
  min-width: 0;
  padding: 12px;
  border-right: 1px solid var(--border);
  overflow-y: auto;
}

.fb-brand {
  margin-bottom: 14px;
  padding: 0 8px;
  font-size: 15px;
  font-weight: 700;
}

/* The account row: who you are, above everything that belongs to you. */
.fb-account {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-width: 0;
  margin-bottom: 10px;
  padding: 6px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.fb-account:hover {
  background: var(--hover);
}

.fb-account-text {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}

.fb-account-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
}

.fb-account-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.fb-search {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  margin-bottom: 10px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-sunken);
  color: var(--text-muted);
}

/* `flex: 1 1 auto` alone is not enough: `min-width: auto` on a flex item
   resolves to an input's min-content size, about 155px, and the field would
   push the rail wider than the rail is. */
.fb-search-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.fb-search-input:focus {
  outline: none;
}

.fb-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.fb-rail-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 16px 0 4px;
  padding: 0 8px;
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.fb-rail-hint {
  margin: 2px 8px;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
}

.fb-scope {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-width: 0;
  padding: 6px 8px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  text-align: left;
  cursor: pointer;
}

.fb-scope:hover {
  background: var(--hover);
}

.fb-scope.is-on {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
  font-weight: 600;
}

.fb-scope-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fb-scope-count {
  flex: 0 0 auto;
  min-width: 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-variant-numeric: tabular-nums;
}

.fb-main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  padding: 16px 24px 24px;
  overflow-y: auto;
}

.fb-topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-bottom: 12px;
}

/* What an import had to say, said inside this screen. The editor's status bar
   sits behind an opaque `.file-browser`, so a message sent only there is a
   message nobody can read — which is how a refused drop came to look like
   nothing happening at all. */
.fb-notice {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 14px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font-size: 12px;
  line-height: 1.5;
}

.fb-notice[data-tone='error'] {
  border-left-color: var(--danger);
}

.fb-notice[data-tone='success'] {
  border-left-color: var(--positive);
}

.fb-notice-text {
  flex: 1 1 auto;
  min-width: 0;
}

.fb-notice-action {
  flex: 0 0 auto;
  align-self: center;
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg-sunken);
  color: var(--text);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  white-space: nowrap;
  cursor: pointer;
}

.fb-notice-action:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.fb-notice-dismiss {
  flex: 0 0 auto;
  border: 0;
  padding: 0 2px;
  background: none;
  color: var(--text-subtle);
  font-size: 12px;
  line-height: 1.5;
  cursor: pointer;
}

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

.fb-crumbs {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-muted);
  font-size: 12px;
}

.fb-crumb.is-current {
  color: var(--text);
}

.fb-crumb-sep {
  color: var(--text-subtle);
}

.fb-topbar-actions {
  display: flex;
  gap: 8px;
}

.fb-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 14px;
}

.fb-title {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
}

.fb-title h2 {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 20px;
}

.fb-header-note {
  flex: 1 1 100%;
  margin: -6px 0 0;
  color: var(--text-muted);
  font-size: 12px;
}

.fb-primary,
.fb-secondary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.fb-primary {
  border-color: var(--accent);
  background: var(--accent-fill);
  color: var(--on-accent);
}

.fb-danger {
  border-color: var(--danger);
  color: var(--danger);
}

/* The template strip, dismissible and staying dismissed. */
.fb-templates {
  margin-bottom: 18px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-sunken);
}

.fb-templates-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.fb-templates-head h3 {
  margin: 0;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
}

.fb-templates-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 12px;
}

.fb-template {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* Drawn in CSS rather than shipped as four images: a thumbnail of a template
   is a picture of a document that does not exist yet, and a gradient that
   hints at the layout is honest where a screenshot of an old build is not. */
.fb-template-art {
  aspect-ratio: 16 / 10;
  margin-bottom: 6px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: linear-gradient(135deg, var(--art-wash-b), var(--art-wash-a));
}

.fb-template-art[data-template='blank'] {
  background: var(--panel);
}

.fb-template-art[data-template='dashboard'] {
  background:
    linear-gradient(90deg, var(--art-ink) 0 24%, transparent 24%),
    linear-gradient(180deg, var(--art-wash-a), var(--art-wash-b));
}

.fb-template-art[data-template='mobile'] {
  background:
    radial-gradient(closest-side, var(--art-accent) 60%, transparent 62%),
    linear-gradient(180deg, var(--art-accent), var(--art-wash-b));
}

.fb-template:hover .fb-template-art {
  border-color: var(--accent);
}

.fb-template-name {
  font-size: 12px;
  font-weight: 600;
}

.fb-template-desc {
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.fb-filters {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.fb-count {
  color: var(--text-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.fb-filters-spacer {
  flex: 1 1 auto;
  min-width: 0;
}

.fb-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 28px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.fb-chip:disabled {
  color: var(--text-subtle);
  cursor: default;
}

.fb-viewtoggle {
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 8px;
}

.fb-viewtoggle .is-on {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

.fb-results {
  flex: 1 1 auto;
  min-height: 0;
}

/* Sized by the thumbnails, not by a fixed column count: at 1200px this is five
   across and at 700px it is two, with no breakpoint to maintain. */
.fb-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px;
}

/* The height of the cards that were not built. A whole row of the grid rather
   than a cell in it, or the reserved space would sit between four cards
   instead of above them. Zero-height by default so an empty spacer costs
   nothing but a grid line. */
/* Figma's keyframe diamond, on the row it keys (bug 495). Three states, and
   the difference between them has to survive the icon font being absent — the
   fill weight carries it, and `icons.ts` gives the filled cut its own fallback
   character for exactly this reason. */
.field-keyed {
  display: flex;
  align-items: center;
  /* 2px, and the diamond below is 20px rather than 24. The inspector column is
     264px and the Dimensions row already carries two number boxes, two sizing
     menus and the ratio lock; at 24 + 4 the `W` box was still five pixels short
     of its own content, which `probe-motion-keys` measures rather than trusts
     to the eye. */
  gap: 2px;
  /* Inside a `.field-pair` this stands where a `.field` used to, and the pair
     sizes its children with `flex: 1 1 0` on `.field` alone — so without this
     the wrapper kept its content's natural width, the pair overflowed its
     264px column, and the diamond was drawn under the neighbouring control.
     `probe-motion-keys` found it on its first run by asking
     `elementFromPoint` rather than `querySelector`: all four diamonds existed
     and none of them could be pressed. Bug 465's class, again. */
  flex: 1 1 0;
  min-width: 0;
}

.field-keyed > :first-child {
  flex: 1 1 auto;
  min-width: 0;
}

/* And the wrapper is what the pair's own rules have to reach through, since
   the `.field` they were written for is now one level down. */
.field-pair > .field-keyed > .field {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  flex: 1 1 0;
  margin-bottom: 6px;
}

.motion-key {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  /* Dim until the property is animated: an unanimated row has a diamond that
     invites rather than reports, and one at full contrast on every row would
     make the panel read as a wall of controls. */
  color: var(--text-subtle);
  cursor: pointer;
}

.motion-key:hover {
  background: var(--hover);
  color: var(--text);
}

.motion-key.is-animated {
  color: var(--accent);
}

.motion-key.is-keyed {
  color: var(--accent);
  background: var(--accent-soft);
}

.fb-spacer {
  grid-column: 1 / -1;
  height: 0;
}

.fb-spacer-row td {
  height: 0;
  padding: 0;
  border: 0;
}

/* Several files ticked, and what can be done with them. Above the results
   rather than floating over them: it appears the moment something is ticked,
   and a bar that covers a row of cards would hide the files being chosen. */
.fb-picked-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
  padding: 8px 12px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--panel);
}

.fb-picked-count {
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
  color: var(--text);
}

.fb-picked-action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  min-height: 28px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.fb-picked-action:hover {
  background: var(--field-hover);
}

.fb-picked-clear {
  border: 0;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.fb-picked-clear:hover {
  color: var(--text);
}

/* A ticked file. The ring is on the thumbnail rather than on the card, so it
   reads as "this picture is chosen" rather than as a hover state on a column
   of text. */
.fb-card.is-picked .fb-thumb,
.fb-row.is-picked {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.fb-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

/* Always in the DOM, revealed on hover or when it is on — a star that appears
   only on hover is one a touch device can never reach. */
.fb-star {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 1;
  border-radius: 6px;
  background: var(--panel);
  opacity: 0;
}

.fb-card:hover .fb-star,
.fb-star:focus-visible,
.fb-star.is-on {
  opacity: 1;
}

.fb-star.is-on {
  color: var(--warning);
}

.fb-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  width: 100%;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  cursor: pointer;
  overflow: hidden;
}

.fb-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.fb-thumb:hover {
  border-color: var(--accent);
}

.fb-card.is-open .fb-thumb {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.fb-card-foot {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.fb-card-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 500;
}

.fb-card-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
}

.fb-card-when {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* The list layout. A table, so the dates line up — which is the only reason
   to want a list instead of the cards. */
.fb-list {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

.fb-list th {
  padding: 0 8px 8px;
  border-bottom: 1px solid var(--border);
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-weight: 600;
  text-align: left;
}

.fb-list td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--border);
}

.fb-row:hover {
  background: var(--hover);
}

.fb-row.is-open .fb-row-name {
  color: var(--accent);
  font-weight: 600;
}

.fb-row-open {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-width: 0;
  padding: 4px 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.fb-row-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fb-row-star {
  color: var(--warning);
}

.fb-row-where,
.fb-row-when {
  color: var(--text-muted);
  white-space: nowrap;
}

.fb-row-actions {
  width: 32px;
  text-align: right;
}

.fb-rename {
  flex: 1 1 auto;
  min-width: 0;
  height: 22px;
  padding: 0 5px;
  border: 1px solid var(--accent);
  border-radius: 4px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.fb-empty {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 40px 0;
  color: var(--text-muted);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

/* --- Swapping an instance's component ------------------------------------- */

.swap-target {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1 1 auto;
  /* The flexbox trap again: without this the button's min-content width is its
     longest component name, and a long one pushes the label out of the panel. */
  min-width: 0;
  height: 24px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.swap-target:hover {
  border-color: var(--accent);
}

.swap-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

.swap-popover {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 260px;
  padding: 8px;
}

.swap-search {
  width: 100%;
  min-width: 0;
  height: 26px;
  padding: 0 8px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

/* Bounded, and scrolling: a library of two hundred components would otherwise
   make a popover taller than the screen, and the clamp would put the search
   field off the top of it. */
.swap-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-height: 260px;
  overflow-y: auto;
}

.swap-option {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-width: 0;
  padding: 5px 6px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.swap-option:hover {
  background: var(--hover);
}

.swap-option.is-on {
  color: var(--accent);
}

.swap-folder {
  flex: 0 0 auto;
  color: var(--text-subtle);
}

.swap-leaf {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.swap-tick {
  flex: 0 0 auto;
}

.swap-empty {
  margin: 6px 4px;
  color: var(--text-muted);
  font-size: 12px;
}

/* --- Sharing --------------------------------------------------------------
   The corner of the toolbar Figma reserves for "who else is here" and "let
   somebody else in", in that order: the avatars answer a question you have
   while working, the button answers one you have deliberately. */

.toolbar-presence {
  display: flex;
  align-items: center;
  gap: -4px;
  margin-right: 8px;
}

/* Overlapped, like every collaborator row: four avatars in the width of three,
   and the overlap is what makes it read as a group rather than as four
   unrelated dots. */
.presence-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin-left: -6px;
  border: 2px solid var(--panel);
  border-radius: 50%;
  background: var(--peer-colour, var(--identity-0));
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  user-select: none;
}

.presence-avatar.is-more {
  background: var(--border-strong);
  color: var(--text);
}

.share-button {
  height: 28px;
  padding: 0 14px;
  border: 0;
  border-radius: 8px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

/* A live session is the one state the button itself has to carry: the avatars
   disappear when you are connected and alone, and "sharing is on" is not
   something to find out by opening the dialog. */
.share-button.is-live {
  box-shadow: 0 0 0 2px var(--accent-soft);
}

/* --- who you are, at the right end of the toolbar ------------------------- */

/* Past Share, which is where every tool with an account puts it. There used to
   be nothing here at all: signing in to the workspace lived behind File →
   Server, and the *other* account system had a full-screen gate before the
   canvas — the thing that could do everything was hidden and the thing that
   could do nothing was unavoidable. */
.toolbar-identity {
  display: flex;
  align-items: center;
  margin-left: 8px;
}

/* A word, not a face: an avatar for somebody no server has heard of is a claim
   rather than a person. Outlined rather than filled so it does not compete with
   Share, which is the button somebody in a file is more likely to want. */
.identity-signin {
  height: 28px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.identity-signin:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.identity-button {
  display: flex;
  align-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
}

.identity-button:hover .avatar,
.identity-button:focus-visible .avatar {
  box-shadow: 0 0 0 2px var(--accent-soft);
}

.share-backdrop {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 80px 24px 24px;
  background: var(--overlay);
  overflow-y: auto;
}

.share-dialog {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-width: 520px;
  padding: 20px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.share-header {
  display: flex;
  align-items: center;
  gap: 10px;
}

.share-header h2 {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 16px;
}

.share-copy {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.share-copy:disabled {
  color: var(--text-subtle);
  cursor: default;
}

.share-join {
  display: flex;
  gap: 8px;
}

/* `min-width: 0`, as everywhere: an input's min-content width is about 155px
   and it would push the Join button off the dialog on a narrow window. */
.share-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 34px;
  padding: 0 10px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

.share-primary {
  height: 34px;
  padding: 0 16px;
  border: 0;
  border-radius: 8px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
  cursor: pointer;
}

.share-secondary {
  height: 28px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

/* The link itself, shown rather than only copyable. */
.share-link {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
}

/* `min-width: 0` and `text-overflow`, because a share link is long: without
   them the input is as wide as its content and stretches the whole dialog past
   the edge of the window, taking the Copy button with it. Read-only rather
   than disabled — a disabled input cannot be selected, and selecting the text
   is the fallback for every browser that refuses the clipboard. */
.share-link-url {
  min-width: 0;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-overflow: ellipsis;
}

.share-link-detail {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
}

/* The drop target over the file browser.
   Present at all times and hidden by opacity rather than inserted on drag: an
   element added during a drag changes what is under the pointer, and the
   browser answers that with a `dragleave` that cancels the drag. */
.fb-drop {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
  z-index: 40;
}

.file-browser.is-dropping .fb-drop,
.fb-drop.is-working {
  opacity: 1;
}

.fb-drop-sheet {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
  padding: 28px 40px;
  border: 2px dashed var(--accent);
  border-radius: 16px;
  background: var(--panel);
  color: var(--text);
  font-size: 15px;
  text-align: center;
}

.fb-drop-sheet span {
  color: var(--text-muted);
  font-size: 12px;
}

/* The caveat beside an import's score: what the number could not count. */
.import-caveat {
  color: var(--warning);
}

/* Shown only when the account has more than one project to choose between. */
.share-project {
  height: 32px;
  padding: 0 8px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.share-hint,
.share-note {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
}

.share-note {
  color: var(--text-subtle);
  padding-top: 4px;
  border-top: 1px solid var(--border);
}

/* What the last invite did. Two tones, because "Dana can now edit this file"
   and "nobody here has that address" must not look the same at a glance. */
.share-note[data-tone='good'] {
  color: var(--positive);
}

.share-note[data-tone='bad'] {
  color: var(--danger);
}

/* Inviting somebody. One row: address, what they may do, and the button. */
.share-invite {
  display: flex;
  align-items: center;
  gap: 8px;
}

.share-invite-email {
  flex: 1 1 auto;
  min-width: 0;
  height: 30px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font-size: 12px;
}

.share-invite-email:focus {
  outline: none;
  border-color: var(--accent);
}

.share-invite-role {
  flex: 0 0 auto;
  height: 30px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font-size: 12px;
}

.share-dialog h3 {
  margin: 4px 0 0;
  font-size: 12px;
  font-weight: 600;
}

.share-access {
  display: flex;
  flex-direction: column;
}

.share-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}

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

.share-row-text {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}

.share-row-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

.share-row-detail {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.share-row-right {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-size: 12px;
}

/* The peer's own colour, the same one their cursor is drawn in on the canvas.
   Two different colours for one person would make the list useless for the
   thing it is for: matching a name to the cursor that is moving. */
.share-dot {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--peer-colour, var(--identity-0));
}

/* An invitation that has not been answered. Hollow rather than filled: the
   row is a person who is not here yet, and a solid dot the same size as a
   member's puts them in the room a letter has not yet reached. */
.share-dot-pending {
  background: transparent;
  border: 1px dashed var(--border-strong);
}

.share-state {
  display: flex;
  align-items: center;
  gap: 8px;
}

.share-status {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-muted);
  font-size: 12px;
}

.share-status[data-state='connected'] {
  color: var(--positive);
}

.share-status[data-state='reconnecting'],
.share-status[data-state='connecting'] {
  color: var(--text);
}

/* The avatar group is one control, not four decorations: it opens the people
   menu, which is where following and the spotlight live. */
/* --------------------------------------------------------- the meeting strip

   A call is on. Absent rather than empty when there is none — the same
   decision the timeline strip made, and for the same reason: a design file
   nobody has called a meeting in should carry no bar and no repaint.

   The recording mark is the part that earns its colour. Whether a room is
   being recorded is the one fact in this feature nobody must ever have to
   guess at, so it is a word on screen in the danger colour for as long as it
   is true, not a toast at the moment it changed. */
.meeting-strip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 28px;
  padding: 0 6px 0 10px;
  margin-right: 4px;
  border-radius: 14px;
  background: var(--bg-sunken);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.meeting-strip.is-recording {
  background: var(--danger-soft);
}

.meeting-recording {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--danger);
}

/* The dot, drawn rather than an icon: a red circle beside the word is what
   every recorder in the world uses, and it needs no font to arrive. */
.meeting-recording::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--danger);
}

.meeting-label {
  color: var(--text-muted);
}

.meeting-action {
  height: 20px;
  padding: 0 8px;
  border: 0;
  border-radius: 10px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: var(--type-body-small);
  letter-spacing: var(--type-body-small-tracking);
  cursor: pointer;
}

.meeting-action:hover {
  filter: brightness(1.08);
}

/* The share button while it is publishing: the same control saying "press me
   to stop", so it must not read as the ordinary offer to start. */
.meeting-action.is-live {
  background: var(--danger);
}

/* ------------------------------------------------------- watching a share

   Over the canvas, in the corner, small — see `meeting-view.ts`. A share that
   took the window would leave the person watching unable to point at anything,
   which is the whole argument for sharing from inside the design tool. */
.meeting-view {
  position: absolute;
  right: 16px;
  bottom: 72px;
  z-index: 6;
  width: 360px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.meeting-view-header {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 28px;
  padding: 0 6px 0 10px;
}

.meeting-view-who {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  color: var(--text-muted);
  font-size: var(--type-body-small);
  letter-spacing: var(--type-body-small-tracking);
  white-space: nowrap;
  text-overflow: ellipsis;
}

.meeting-view-close {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  line-height: 1;
  cursor: pointer;
}

.meeting-view-close:hover {
  background: var(--hover);
  color: var(--text);
}

/* Black, and letterboxed rather than cropped: the sharer's aspect is theirs,
   and cutting the edges off would hide exactly what somebody is pointing at. */
.meeting-view-video {
  display: block;
  width: 100%;
  height: 202px;
  background: #000;
  object-fit: contain;
}

.presence-group {
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 0 6px 0 10px;
  border: 0;
  border-radius: 16px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.presence-group:hover {
  background: var(--hover);
}

.presence-avatar.is-me {
  background: var(--border-strong);
  color: var(--text);
}

/* The ring says "your screen is following this person" — on the avatar, not
   only in the menu, because the menu is shut while you are being moved. */
.presence-avatar.is-followed {
  box-shadow: 0 0 0 2px var(--accent);
}

.presence-caret {
  margin-left: 4px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1;
}

/* --- The vector toolbar ---------------------------------------------------
   Figma's, at the bottom of the canvas while points are being edited, and
   with only the modes that do something: a row offering five things of which
   two are unimplemented is worse than a row offering three that work. */

.vector-toolbar {
  position: absolute;
  bottom: 76px;
  left: 50%;
  z-index: 12;
  display: flex;
  align-items: center;
  /* One row, always. Without this the mirroring group and the hint wrap onto
     lines of their own and the bar becomes a paragraph. */
  flex-wrap: nowrap;
  white-space: nowrap;
  gap: 2px;
  padding: 4px;
  border-radius: 12px;
  background: var(--dock);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
  transform: translateX(-50%);
}

.vector-tool {
  height: 28px;
  padding: 0 12px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--dock-text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.vector-tool:hover {
  background: var(--dock-hover);
}

.vector-tool.is-on {
  background: var(--accent-fill);
  color: var(--on-accent);
}

/* An icon-only tool. The bars were rows of words — "Move  Bend  Pen  Cut
   Lasso", "Arrow  Line  Rectangle  Ellipse  Highlight  Measure  Note" — which
   at seven kinds was wider than the canvas it floated over. A square is what a
   toolbar button is everywhere else in this editor, so the padding goes and
   the width comes from the icon.

   The word is not lost: `interactions.ts` puts it in `title` and in
   `aria-label`, which is the trade that makes an icon bar bearable rather than
   unlearnable. */
.vector-tool.is-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  padding: 0;
}

.vector-hint {
  margin: 0 8px;
  color: var(--dock-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   The annotations list.

   Deliberately the comment list's rows — same class, same spacing, same hover —
   because they are the same kind of thing to scan: a line of markup with a
   place to go. What differs is the two pieces of information a comment does not
   have, and each gets exactly the space it needs: a dot in the annotation's own
   colour, so the list reads like the canvas; and a page heading, because an
   annotation on a page you have not visited is the case this panel exists for.
   --------------------------------------------------------------------------- */
.annotation-group {
  margin: 12px 12px 4px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.annotation-group:first-child {
  margin-top: 4px;
}

.annotation-dot {
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
  margin-right: 6px;
  border-radius: 50%;
}

/* An arrow with no note is a legitimate annotation, and its row still has to be
   clickable — so the words say there is no note rather than the row being
   blank, and they are dimmed because they are this panel's words rather than
   somebody's. */
.comment-excerpt.is-empty {
  color: var(--text-subtle);
  font-style: italic;
}

/* ---------------------------------------------------------------------------
   The annotate bar.

   The same shell as the vector toolbar and the crop bar, for the third time
   and for the same reason: one place for "the controls that belong to the mode
   you are in". What is specific to this one is the swatches and the note
   field, and both are narrow — the bar already carries seven kind buttons and
   has to stay on one line at a laptop width.
   --------------------------------------------------------------------------- */
.annotation-colours {
  display: flex;
  align-items: center;
  gap: 3px;
  margin: 0 4px 0 8px;
  padding-left: 8px;
  border-left: 1px solid rgba(255, 255, 255, 0.16);
}

.annotation-colour {
  width: 18px;
  height: 18px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 50%;
  cursor: pointer;
}

.annotation-colour.is-on {
  /* The ring is outside the swatch, so the colour itself is never covered —
     a selected swatch that dims the colour it names is the one control here
     that has to stay legible. */
  border-color: var(--dock-text);
  box-shadow: 0 0 0 1px var(--dock);
}

/* The note editor, on the canvas.

   This used to be `.annotation-note`, a field in the bar, disabled until
   something was selected. That was the wrong place for it: an annotation says
   something *about a place*, and somebody writing the note was looking at the
   bottom of the window instead of at the thing they were describing. The field
   is now positioned over the annotation itself — `placeAnnotationText` in
   `interactions.ts` moves it on every overlay frame, so it follows a pan, a
   zoom, and the layer an anchored annotation travels with.

   Panel colours rather than dock colours, because it is no longer part of a
   dark floating bar: it sits on the artwork, and it has to be readable against
   whatever is under it. The left border takes the annotation's own colour,
   written inline, which is what ties the caret to the shape it belongs to. */
.annotation-inline {
  position: absolute;
  z-index: 13;
  width: 180px;
  height: 26px;
  padding: 0 8px;
  border: 1px solid var(--border-strong);
  /* The colour of the annotation goes here — see `placeAnnotationText`. */
  border-left-width: 3px;
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  box-shadow: var(--shadow-panel);
}

.annotation-inline::placeholder {
  color: var(--text-subtle);
}

/* ---------------------------------------------------------------------------
   The crop bar.

   The same bar as the vector toolbar — same place, same shape, same colours —
   because it is the same kind of thing: the controls that belong to the mode
   you are in. A second style for a second mode would teach nobody anything and
   would be one more thing to keep in step with the theme.

   The slider is the one control here a gesture cannot replace. It is wide
   enough to be swept usefully: a 60px range has about twelve usable positions,
   and a magnification you cannot land on is a magnification you type instead.
   --------------------------------------------------------------------------- */
.crop-zoom {
  width: 132px;
  margin: 0 6px 0 8px;
  accent-color: var(--accent);
}

.crop-zoom-reading {
  min-width: 42px;
  margin: 0 6px 0 0;
  /* Tabular, so the bar does not twitch sideways while the slider moves and
     the reading goes from 100% to 250%. */
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* ---------------------------------------------------------------------------
   The handle-mirroring group.

   Figma's three settings, separated from the modes by a rule because they are
   a different kind of thing: the modes decide what a click does, these decide
   what a *point* is. Square buttons rather than the modes' pill shape, for the
   same reason.
   --------------------------------------------------------------------------- */
.vector-mirror {
  display: flex;
  align-items: center;
  gap: 2px;
  margin: 0 2px 0 6px;
  padding-left: 8px;
  border-left: 1px solid rgba(255, 255, 255, 0.16);
}

.vector-mirror-button {
  width: 28px;
  min-width: 28px;
  padding: 0;
  font-size: 14px;
  line-height: 1;
  text-align: center;
}

/* ---------------------------------------------------------------------------
   The subpath group.

   Present only on a shape that has more than one run, unlike the mirroring
   group beside it — a subpath control on a path with no subpaths could never
   do anything, and a permanently dead button teaches people the row is
   decoration. The reading is a count rather than a name because runs have no
   names: a Subtract made them and nobody typed anything.
   --------------------------------------------------------------------------- */
.vector-subpath {
  display: flex;
  align-items: center;
  gap: 2px;
  margin: 0 2px;
  padding-left: 8px;
  border-left: 1px solid rgba(255, 255, 255, 0.16);
}

.vector-subpath-label {
  margin-right: 4px;
  color: var(--dock-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.vector-subpath-button {
  width: 28px;
  min-width: 28px;
  padding: 0;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  line-height: 1;
  text-align: center;
}

/* Disabled, not hidden: a control that comes and goes with the selection is
   one nobody learns is there. It has to *look* unavailable, though, or the
   first press reads as the button being broken. */
.vector-tool:disabled {
  opacity: 0.38;
  cursor: default;
}

.vector-tool:disabled:hover {
  background: none;
}

.vector-done {
  color: var(--dock-muted);
}

/* ---------------------------------------------------------------------------
   The comment thread popover.

   DOM rather than canvas, and positioned absolutely inside the canvas host so
   it moves with a pan without anybody re-laying-out the page. The pins are
   painted; this is the one thread you have open, and it is a panel because you
   type into it.
   --------------------------------------------------------------------------- */
.comment-popover {
  position: absolute;
  z-index: 14;
  display: flex;
  width: 268px;
  max-height: 60vh;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
  color: var(--text);
  font-size: 12px;
}

.comment-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 8px 6px 12px;
  border-bottom: 1px solid var(--border);
}

.comment-title {
  overflow: hidden;
  flex: 1;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.comment-resolve,
.comment-close {
  height: 24px;
  padding: 0 8px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.comment-resolve:hover,
.comment-close:hover {
  background: var(--accent-soft);
  color: var(--text);
}

/* Scrolls rather than growing: a thread of forty replies must not push the
   composer off the bottom of the window, which is where the reply goes. */
.comment-messages {
  overflow-y: auto;
  flex: 1;
  padding: 6px 12px;
}

.comment-message + .comment-message {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* Replies indent, so the first message reads as the subject and the rest as
   the conversation about it. */
.comment-message.is-reply {
  padding-left: 10px;
  border-left: 2px solid var(--border-strong);
}

.comment-meta {
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.comment-author {
  font-weight: 600;
}

.comment-time,
.comment-edited {
  color: var(--text-subtle);
  font-size: 10px;
}

.comment-body {
  margin: 3px 0 0;
  /* Somebody else's text, of unknown shape: a pasted URL with no spaces in it
     must wrap rather than widen the panel past the edge of the screen. */
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.comment-reactions {
  display: flex;
  gap: 4px;
  margin-top: 5px;
}

.comment-reaction {
  height: 22px;
  padding: 0 6px;
  border: 1px solid transparent;
  border-radius: 11px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
  opacity: 0.55;
}

.comment-reaction:hover {
  background: var(--accent-soft);
  opacity: 1;
}

.comment-reaction.is-on {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--text);
  opacity: 1;
}

.comment-composer {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  padding: 8px 10px 10px;
  border-top: 1px solid var(--border);
}

.comment-input {
  min-height: 34px;
  flex: 1;
  padding: 6px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  resize: vertical;
}

.comment-input:focus {
  border-color: var(--accent);
  outline: none;
}

.comment-send {
  height: 30px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.comment-send:hover {
  filter: brightness(1.08);
}

/* ---------------------------------------------------------------------------
   The multi-selection header.

   A count, the boolean split button, and the overflow — Figma's row. The split
   button is one pill with a hairline down the middle rather than two buttons,
   so it reads as "one control with a menu" instead of two unrelated icons.
   --------------------------------------------------------------------------- */
.inspector-count {
  overflow: hidden;
  flex: 1;
  font-size: 12px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.inspector-split {
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  border-radius: 6px;
}

.inspector-split .icon-button {
  border-radius: 0;
}

.inspector-split-caret {
  width: 18px;
  min-width: 18px;
  padding: 0;
  border-left: 1px solid var(--border);
}

/* Disabled, not hidden. Two shapes that cannot be combined is a fact about the
   selection, and a button that vanishes is one nobody learns is there. */
.inspector-split-face:disabled,
.inspector-split-caret:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ---------------------------------------------------------------------------
   The file's colour styles, in the empty inspector.

   A dense list rather than the inspector's usual field rows: this is something
   to scan, not something to fill in, and forty styles at field spacing would
   be a page and a half of scrolling.
   --------------------------------------------------------------------------- */
.style-group {
  padding: 2px 12px 4px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.style-list {
  display: flex;
  flex-direction: column;
}

.style-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 12px;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.style-row:hover {
  background: var(--bg-sunken);
}

/* A ring rather than a bare square: a white style on a white panel is
   otherwise an empty gap with a name beside it. */
.style-swatch {
  width: 16px;
  min-width: 16px;
  height: 16px;
  flex: 0 0 16px;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
}

.style-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   A component's declared properties.

   One row per property: its name (editable in place), its kind, and — for a
   variant — the values it may take. Renaming is a click rather than a dialog,
   because the name is the only part of a new property that cannot be guessed.
   --------------------------------------------------------------------------- */
.property-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 12px;
}

.property-name,
.property-options {
  min-width: 0;
  flex: 1 1 auto;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.property-name:focus,
.property-options:focus {
  border-color: var(--accent);
  outline: none;
}

/* The kind is a label, not a control: a property's type is decided when it is
   created and changing it would invalidate every instance's answer. */
.property-kind {
  flex: 0 0 auto;
  color: var(--text-subtle);
  font-size: 10px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* What a property drives. A count rather than a list of layer names, so the
   chip keeps one width whether the property drives one layer or eleven and the
   name field beside it does not move as targets are added. */
.property-targets {
  flex: 0 0 auto;
  padding: 3px 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 10px;
  white-space: nowrap;
  cursor: pointer;
}

.property-targets:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* While the layers are being picked. The lead reads as instruction rather than
   as a label, so it is full width and wraps; the count is what changes as you
   click, so it is the emphatic line of the three. */
.picker-lead,
.picker-count,
.picker-warning {
  margin: 0;
  padding: 2px 12px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.45;
}

.picker-lead {
  color: var(--text-muted);
}

.picker-count {
  color: var(--text);
  font-weight: 600;
}

.picker-warning {
  color: var(--danger);
}

/* The documentation link: an address you can edit and, when it is one the
   editor will follow, a way to follow it. `Open` is a link rather than a button
   because it navigates — middle-click and "copy link address" should both do
   what they do anywhere else. */
.docs-field {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  flex: 1 1 auto;
}

.docs-url {
  min-width: 0;
  flex: 1 1 auto;
}

.docs-open {
  flex: 0 0 auto;
  color: var(--accent);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-decoration: none;
  white-space: nowrap;
}

.docs-open:hover {
  text-decoration: underline;
}

/* ---------------------------------------------------------------------------
   Version history
   --------------------------------------------------------------------------- */

/* A history that cannot be rebuilt. Tinted with `--danger` rather than shown as
   an ordinary note: every row below it is a state the file cannot be taken back
   to, and a person who reads past this will click one and get an error about
   node ids. */
.versions-damaged {
  margin: 0 12px 8px;
  padding: 8px 10px;
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  background: var(--danger-soft);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.versions-damaged-text {
  margin: 0;
  color: var(--text);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.45;
}

.versions-repair {
  align-self: flex-start;
  padding: 4px 10px;
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  background: transparent;
  color: var(--danger);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.versions-repair:hover {
  background: var(--danger);
  color: var(--on-danger);
}

.versions-panel {
  padding: 0;
  overflow-y: auto;
}

.versions-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.versions-header {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 8px 8px 6px;
  background: var(--bg);
}

.versions-save {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 28px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.versions-save:hover:not(:disabled) {
  background: var(--bg-sunken);
  border-color: var(--border-strong, var(--border));
}

.versions-save:disabled {
  opacity: 0.5;
  cursor: default;
}

/* The name field replaces the button in place, so the header must not jump. */
.versions-name-field {
  width: 100%;
  min-width: 0;
  min-height: 28px;
  padding: 0 8px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

/* The name and the note, stacked. A note beside the name would be a second
   single-line field in a 264px column, which is a field nobody writes a
   sentence in — and a sentence is the whole point of asking. */
.versions-name-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
}

.versions-note-field {
  width: 100%;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  line-height: 1.4;
  /* Vertical only: a textarea that can be dragged wider than the sidebar
     escapes the panel and covers the canvas. */
  resize: vertical;
}

.versions-note-field:focus {
  border-color: var(--accent);
  outline: none;
}

.versions-empty {
  margin: 4px 10px 10px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
}

.versions-list {
  display: flex;
  flex-direction: column;
  padding: 0 4px 8px;
}

.versions-day {
  padding: 8px 6px 4px;
  color: var(--text-subtle);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.version-row {
  display: flex;
  align-items: center;
  gap: 2px;
  border-radius: var(--radius);
}

.version-row:hover {
  background: var(--bg-sunken);
}

/* `center`, not `baseline`. The title is 12px and the timestamp is 10px, and a
   baseline-aligned pair of different sizes leaves the free space of the row
   below them — so every row's text sat high in its own box and the list read as
   though it had been nudged. Reported by the owner with a screenshot of it.
   The two sizes still read as one line because the smaller one is centred
   against the larger, which is what the eye is doing anyway. */
.version-main {
  display: flex;
  flex: 1;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 28px;
  padding: 4px 6px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.version-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A named version is the one somebody meant to come back to. */
.version-row.is-named .version-title {
  font-weight: 600;
}

/* The note sits under the name rather than beside it, so it can be a sentence.
   `flex-basis: 100%` after a wrap is what puts it on its own line without a
   second container: the title and the time keep their row, and the note takes
   the next one. */
.version-main.has-note {
  flex-wrap: wrap;
}

.version-note {
  flex: 0 0 100%;
  margin-top: 2px;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.4;
  white-space: normal;
  text-align: left;
}

.version-meta {
  flex: none;
  color: var(--text-subtle);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}

/* Hidden until the row is under the pointer, so a long list stays a list. */
.version-more {
  flex: none;
  opacity: 0;
}

.version-row:hover .version-more,
.version-more:focus-visible {
  opacity: 1;
}

.versions-more {
  margin: 0 8px 10px;
  min-height: 26px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

/* ---------------------------------------------------------------------------
   Branches and merging
   --------------------------------------------------------------------------- */

.branch-bar {
  margin-bottom: 6px;
}

.branch-current {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 26px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.branch-current:hover {
  background: var(--bg-sunken);
}

.branch-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.branch-caret {
  flex: none;
  color: var(--text-subtle);
  font-size: var(--type-body-small);
  letter-spacing: var(--type-body-small-tracking);
}

.merge-sheet {
  position: fixed;
  z-index: 90;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(560px, 92vw);
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.merge-sheet h3 {
  margin: 0 0 10px;
  font-size: 14px;
}

.merge-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 12px;
}

.merge-count {
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--bg-sunken);
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.merge-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.merge-clean {
  margin: 0;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.5;
}

.merge-conflict {
  padding: 10px 0;
  border-top: 1px solid var(--border);
}

.merge-detail {
  margin: 0 0 8px;
  font-size: 12px;
  line-height: 1.5;
}

.merge-choices {
  display: flex;
  gap: 6px;
}

.merge-choice {
  min-height: 26px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

/* The chosen side, which is the only state that says the conflict is answered. */
.merge-choice.active {
  border-color: var(--accent);
  background: var(--accent-fill);
  color: var(--on-accent);
}

.merge-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 14px;
}

.merge-cancel,
.merge-confirm {
  min-height: 28px;
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.merge-confirm {
  border-color: var(--accent);
  background: var(--accent-fill);
  color: var(--on-accent);
}

.merge-confirm:disabled {
  opacity: 0.5;
  cursor: default;
}

/* ---------------------------------------------------------------------------
   Comments panel
   --------------------------------------------------------------------------- */

.comments-panel {
  padding: 0;
  overflow-y: auto;
}

.comments-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.comments-filters {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  gap: 4px;
  padding: 8px;
  background: var(--bg);
}

.comments-filter {
  display: flex;
  align-items: center;
  gap: 5px;
  min-height: 24px;
  padding: 0 8px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--bg-sunken);
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.comments-filter.active {
  border-color: var(--border);
  background: var(--bg);
  color: var(--text);
  font-weight: 600;
}

.comments-unread {
  min-width: 15px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.comments-empty {
  margin: 4px 10px 10px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
}

.comments-list {
  display: flex;
  flex-direction: column;
  padding: 0 4px 8px;
}

.comment-row {
  display: flex;
  align-items: flex-start;
  gap: 2px;
  border-radius: var(--radius);
}

.comment-row:hover {
  background: var(--bg-sunken);
}

.comment-main {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: 6px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.comment-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
}

.comment-author {
  overflow: hidden;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.comment-when {
  flex: none;
  color: var(--text-subtle);
  font-size: 10px;
}

/* Two lines, then an ellipsis: the list is for finding a thread, not reading it. */
.comment-excerpt {
  display: -webkit-box;
  overflow: hidden;
  color: var(--text-muted);
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.comment-row-meta {
  color: var(--text-subtle);
  font-size: 10px;
}

/* Unread is a bar down the left edge, not a colour on the text: the text has to
   stay as readable as every other row. */
.comment-row.is-unread {
  box-shadow: inset 2px 0 0 var(--accent);
}

.comment-row.is-resolved .comment-author,
.comment-row.is-resolved .comment-excerpt {
  opacity: 0.6;
}

.comment-row.is-orphaned .comment-row-meta {
  color: var(--warning);
}

.comment-more {
  flex: none;
  margin-top: 4px;
  opacity: 0;
}

.comment-row:hover .comment-more,
.comment-more:focus-visible {
  opacity: 1;
}

/* The @ picker, over the composer rather than pushing it down: a list that
   reflows the textarea moves the caret out from under the pointer. */
.mention-list {
  position: absolute;
  z-index: 3;
  bottom: 100%;
  left: 8px;
  right: 8px;
  max-height: 148px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.mention-option {
  display: block;
  width: 100%;
  min-height: 26px;
  padding: 0 8px;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.mention-option.active,
.mention-option:hover {
  background: var(--bg-sunken);
}

.comment-composer {
  position: relative;
}

/* ---------------------------------------------------------------------------
   Notifications — the bell and its panel
   --------------------------------------------------------------------------- */

.toolbar-bell {
  display: flex;
  align-items: center;
}

.bell-button {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 28px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.bell-button:hover,
.bell-button.has-unread {
  color: var(--text);
}

/* The count sits on the bell's shoulder, as every product with one puts it. */
.bell-badge {
  position: absolute;
  top: 0;
  right: 0;
  min-width: 15px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--danger);
  color: var(--on-danger);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  line-height: 15px;
  text-align: center;
}

.bell-panel {
  position: fixed;
  z-index: 85;
  left: var(--bell-left, auto);
  top: var(--bell-top, 48px);
  display: flex;
  flex-direction: column;
  width: 360px;
  max-height: min(560px, 70vh);
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.bell-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px 10px;
}

.bell-head h3 {
  margin: 0;
  font-size: 15px;
}

.bell-clear {
  border: 0;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.bell-clear:disabled {
  color: var(--text-subtle);
  cursor: default;
}

.bell-tabs {
  display: flex;
  gap: 4px;
  padding: 0 12px;
  border-bottom: 1px solid var(--border);
}

.bell-tab {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 6px 10px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.bell-tab.active {
  color: var(--text);
  font-weight: 600;
}

/* The underline is the tab marker, drawn inside the strip so the border below
   it is not doubled. */
.bell-tab.active::after {
  content: '';
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: -1px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}

.bell-tab-count {
  min-width: 16px;
  padding: 0 4px;
  border-radius: 4px;
  background: var(--bg-sunken);
  color: var(--text-muted);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.bell-list {
  flex: 1;
  min-height: 120px;
  overflow-y: auto;
  padding: 6px;
}

.bell-empty {
  margin: 0;
  padding: 48px 0;
  color: var(--text-subtle);
  font-size: 12px;
  text-align: center;
}

.bell-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  padding: 10px 10px 10px 18px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.bell-row:hover {
  background: var(--bg-sunken);
}

/* Unread is a dot, not a colour on the words: the text has to stay as readable
   as every other row's. */
.bell-dot {
  position: absolute;
  top: 16px;
  left: 7px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: transparent;
}

.bell-row.is-unread .bell-dot {
  background: var(--danger);
}

.bell-avatar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-size: 12px;
  font-weight: 600;
}

.bell-avatar.is-system {
  background: var(--bg-sunken);
  color: var(--text-muted);
}

.bell-text {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.bell-title {
  font-size: 12px;
  font-weight: 600;
}

.bell-body {
  display: -webkit-box;
  overflow: hidden;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.45;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
}

.bell-when {
  color: var(--text-subtle);
  font-size: 10px;
}

/* ------------------------------------------------------- the file identity

  The name of the file and the menu that acts on it, together in the top-left
  corner — the one place a person looks to answer "what am I in". The caret is
  a separate target from the name because "go back to my files" and "do
  something to this file" are different intentions, and one button cannot be
  both. */

.doc-identity {
  display: flex;
  align-items: center;
  gap: 2px;
  min-width: 0;
}

.doc-menu {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text-subtle);
  cursor: pointer;
}

.doc-menu:hover,
.doc-menu[aria-expanded='true'] {
  background: var(--hover);
  color: var(--text);
}

/* Minimize/Expand UI — the sidebar-toggle button beside the file name. Same
   shape as the file-menu caret so the two read as one cluster; a little space
   to its left separates "act on the file" from "clear the panels away". */
.minimize-ui {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin-left: 6px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-subtle);
  cursor: pointer;
}

.minimize-ui:hover,
.minimize-ui[aria-pressed='true'] {
  background: var(--hover);
  color: var(--text);
}

/* --------------------------------------------------- panel header controls

  A heading that carries a control needs room for it; the plain `.sidebar-heading`
  is a label and packs to the left. Both of these push their trailing control to
  the far edge, which is where a list's "add" and a view's zoom belong. */

.inspector-heading {
  /* The zoom is the only thing left in here — the mode tabs beside it became a
     row of icons in the dock (update 194) — so it goes to the right-hand end,
     which is the corner every design tool puts it in and where it was reached
     for while the tabs held the left. `space-between` with one child puts it on
     the left instead, which is what this said before. */
  justify-content: flex-end;
  gap: 8px;
  padding-right: 6px;
}

/**
 * The two list headings — Pages and Layers.
 *
 * The owner's report, update 200: a heading with a name on it and nothing else
 * is a title, and both of these lists have things people do to them. The
 * chevron folds the section, the label takes whatever room is left, and the
 * buttons sit at the far edge — which is where a list's "add" belongs and where
 * both were reached for.
 */
.pages-heading,
.layers-heading {
  justify-content: flex-start;
  gap: 2px;
  padding-left: 6px;
  padding-right: 6px;
}

.pages-heading > span,
.layers-heading > span {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chevron. Its own class so the fold can be found by a probe and by a
   person reading the DOM, rather than being "the first icon button". */
.heading-fold {
  flex: 0 0 auto;
}

/* The search box replaces the label rather than sitting beside it: a 248px
   heading has room for one of the two, and a field squeezed in next to a word
   is a field you cannot read what you typed into. */
.heading-search {
  flex: 1 1 auto;
  min-width: 0;
  height: 22px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: normal;
  text-transform: none;
}

/**
 * A folded section keeps its heading and loses everything else.
 *
 * `display: none` on the body rather than `hidden` on the element, because a
 * class's own `display` beats the browser's `[hidden]` and the two would fight
 * (bug 440) — and the fold is a fact about this section rather than about the
 * panel, so the class is on the section.
 */
.pages-section.is-folded,
.layers-section.is-folded > .panel-body {
  display: none;
}

/**
 * The Layers panel and its heading, as one thing the rail can hide.
 *
 * `[hidden]` needs saying out loud here for the reason above: this rule sets a
 * `display`, so without the second one a hidden section would still be on
 * screen — which is the whole of bug 440.
 */
.layers-section {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.layers-section[hidden] {
  display: none;
}

.inspector-heading-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The zoom. Moved here from the tool dock: this is the corner every design
   tool puts it in, and it is the one number in the chrome that is about the
   view rather than about the selection. */
.inspector-zoom {
  /* The caret is part of the control now (bug 494), so the chip is a row
     rather than a label: without the flex the icon font's own line box makes
     the button 4px taller than the mode tabs beside it. The caret takes the
     button's own size — `.mode-picker` beside it does the same, and a
     `font-size` literal here would be a new hand-written step against the
     ratchet in `type-scale.test.ts`. */
  display: inline-flex;
  align-items: center;
  gap: 2px;
  min-width: 48px;
  height: 22px;
  padding: 0 4px 0 6px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  text-transform: none;
  cursor: pointer;
}

.inspector-zoom-value {
  font-variant-numeric: tabular-nums;
}

.inspector-zoom:hover {
  background: var(--hover);
  color: var(--text);
}

/* ------------------------------------------------------------- libraries

  Publishing this file, and consuming other files. A dialog rather than a
  panel: connecting a library is decided once and changes what every other
  panel contains, so it takes the screen for the moment it needs and leaves. */

.libraries-backdrop {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 64px 24px 24px;
  background: var(--overlay);
  overflow-y: auto;
}

.libraries-dialog {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 860px;
  max-height: min(720px, calc(100vh - 96px));
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.libraries-header {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 10px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}

/* And the recordings dialog's, which is the same heading in a different
   dialog: one declaration rather than a second 16px, because UI3 jumps from
   13px to 24px and a dialog title lives in the hole (lesson 172's ratchet). */
.libraries-header h2,
.recordings-header h2 {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 16px;
}

.libraries-close {
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  cursor: pointer;
}

.libraries-close:hover {
  background: var(--hover);
  color: var(--text);
}

.libraries-body {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}

.libraries-rail {
  display: flex;
  flex: 0 0 220px;
  /* The reset a fixed flex basis always needs: `min-width: auto` lets a long
     library name push the rail wider than its own basis. */
  min-width: 0;
  flex-direction: column;
  gap: 2px;
  padding: 12px 10px;
  border-right: 1px solid var(--border);
  overflow-y: auto;
}

.libraries-rail-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  text-align: left;
  cursor: pointer;
}

.libraries-rail-item:hover {
  background: var(--hover);
  color: var(--text);
}

.libraries-rail-item.active {
  background: var(--hover);
  color: var(--text);
  font-weight: 600;
}

/* The count rides on the tab, because "is anything out of date" is the
   question the dialog is reopened for and a number you must click to see
   cannot answer it. */
.libraries-badge {
  margin-left: auto;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  line-height: 18px;
  text-align: center;
}

.libraries-panel {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 4px;
  padding: 16px 20px;
  overflow-y: auto;
}

.libraries-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-bottom: 14px;
}

.libraries-group h3 {
  margin: 6px 0 2px;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
}

.libraries-note {
  display: flex;
  flex-direction: column;
  gap: 3px;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.5;
}

.libraries-note strong {
  color: var(--text);
  font-size: 12px;
}

.libraries-actions {
  display: flex;
  gap: 8px;
  padding-top: 6px;
}

.libraries-footer {
  display: flex;
  justify-content: flex-end;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* The quiet twin of the primary button: "Look again", "Check the server" —
   actions that ask a question rather than commit to anything. */
.libraries-primary.is-quiet {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
}

.libraries-primary.is-quiet:hover {
  background: var(--hover);
}

.libraries-primary {
  height: 28px;
  padding: 0 12px;
  border: 0;
  border-radius: 6px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.libraries-primary:disabled {
  opacity: 0.4;
  cursor: default;
}

.libraries-plain {
  height: 28px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.libraries-plain:hover {
  background: var(--bg-sunken);
}

.library-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.library-main {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.library-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
}

.library-meta {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.library-connected {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* The assets of one library, indented under it: they belong to the row above
   and an unindented list reads as a second, unrelated one. */
.library-assets {
  display: flex;
  flex-direction: column;
  padding-left: 12px;
}

.library-asset {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 0;
}

.library-asset-name {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
  color: var(--text-muted);
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* What the publication contains, with a tick per asset. Scrolls at about eight
   rows: a file with two hundred components must not push the Publish button off
   the bottom of the dialog, which is the one control this list qualifies. */
.library-contents {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 220px;
  margin-top: 8px;
  padding: 6px 8px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-sunken);
}

.library-contents-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 0 2px;
}

.library-contents-title {
  flex: 1 1 auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.library-check {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 2px;
  color: var(--text-muted);
  font-size: 12px;
  cursor: pointer;
}

.library-check:hover {
  color: var(--text);
}

.library-check input {
  flex: 0 0 auto;
  margin: 0;
  accent-color: var(--accent);
}

/* ---------------------------------------------------------- style picker

  The list that hangs off every section heading, and the dialog that names what
  is on the node. One of each for Fill, Stroke, Typography and Effects — see
  `style-picker.ts` for why they are not four. */

.style-button.is-styled {
  color: var(--accent);
}

.style-picker {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 260px;
  max-height: 420px;
  padding: 8px 0;
  overflow-y: auto;
}

.style-picker-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 0 8px 4px 12px;
}

.style-picker-title {
  font-size: 12px;
  font-weight: 600;
}

.style-search {
  height: 26px;
  margin: 0 8px 4px;
  padding: 0 8px;
  /* The reset every fixed-basis field needs; see the note on `min-width` in
     `stylesheet.test.ts`. */
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

/* The style a node is already wearing. Marked rather than moved to the top:
   a list that reorders itself under the pointer is a list you cannot learn. */
.style-row.active {
  background: var(--hover);
  font-weight: 600;
}

.style-row .style-detail {
  margin-left: auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-variant-numeric: tabular-nums;
}

/* A style with no colour to show — a text or effect style — gets a neutral
   chip rather than an empty ring that reads as "white". */
.style-swatch.is-abstract {
  background: repeating-linear-gradient(
    45deg,
    var(--bg-sunken),
    var(--bg-sunken) 3px,
    var(--border) 3px,
    var(--border) 6px
  );
}

.style-detach {
  margin: 6px 8px 0;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.style-detach:hover {
  background: var(--bg-sunken);
}

.style-empty {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 8px 12px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
}

.style-empty strong {
  color: var(--text);
  font-size: 12px;
}

/* --- the create dialog --- */

.style-dialog-backdrop {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 96px 24px 24px;
  background: var(--overlay);
  overflow-y: auto;
}

.style-dialog {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-width: 360px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.style-dialog-title {
  margin: 0;
  font-size: 14px;
}

/* The preview is the reason the dialog exists: a style is named after the
   value looks right, so the value has to be on screen while it is named. */
.style-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 120px;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

.style-preview.is-chequered {
  background-image:
    linear-gradient(45deg, var(--bg-sunken) 25%, transparent 25%),
    linear-gradient(-45deg, var(--bg-sunken) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--bg-sunken) 75%),
    linear-gradient(-45deg, transparent 75%, var(--bg-sunken) 75%);
  background-position: 0 0, 0 6px, 6px -6px, -6px 0;
  background-size: 12px 12px;
}

.style-preview-colour {
  width: 100%;
  height: 100%;
}

.style-preview-text {
  font-size: 28px;
}

.style-preview-effect {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}

.style-dialog-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.style-dialog-field input {
  height: 30px;
  min-width: 0;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.style-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.style-dialog-confirm {
  height: 30px;
  padding: 0 14px;
  border: 0;
  border-radius: var(--radius);
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.style-dialog-cancel {
  height: 30px;
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

/* The row and the button that edits it. The row is a button, so the menu
   cannot be inside it — nested buttons are invalid and the inner one does not
   get the click everywhere. The wrapper carries both. */
.style-row-wrapper {
  display: flex;
  align-items: center;
}

.style-row-wrapper .style-row {
  flex: 1 1 auto;
  min-width: 0;
}

/* Shown on hover or focus only: a column of ⋯ down a list of forty styles is
   noise that competes with the swatches, which are what the list is read by. */
.style-row-more {
  opacity: 0;
  margin-right: 6px;
}

.style-row-wrapper:hover .style-row-more,
.style-row-more:focus-visible {
  opacity: 1;
}

/* ---------------------------------------------------------- the server dialog

  Sign in, open a file from the server, send this one to it. Sized narrower than
  the libraries dialog: it is a form and two lists, not a catalogue. */

.server-backdrop {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 64px 24px 24px;
  background: var(--overlay);
  overflow-y: auto;
}

.server-dialog {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 560px;
  max-height: min(720px, calc(100vh - 96px));
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.server-header {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 10px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}

.server-header h2 {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 16px;
}

.server-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.server-close:hover {
  background: var(--hover);
  color: var(--text);
}

.server-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 10px;
  padding: 16px 20px 20px;
  overflow-y: auto;
}

.server-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 6px;
}

.server-group h3 {
  margin: 0;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.server-note {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--bg-sunken);
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.45;
}

/* A label above its input rather than beside it: four fields side by side in a
   560px dialog leaves an address field too narrow to read a URL in. */
.server-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.server-field input {
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

.server-field input:focus {
  border-color: var(--accent);
  outline: none;
}

.server-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding-top: 8px;
}

/*
 * "or sign in with" — a divider with a word in it.
 *
 * The rule is drawn with a border on a pseudo-element either side rather than
 * as a background gradient, so it survives a translated label of any length:
 * "или войдите через" is half again as wide as the English and a fixed-width
 * gap would have the text sitting on the line.
 */
.server-or {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 8px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-transform: lowercase;
}

.server-or::before,
.server-or::after {
  content: '';
  flex: 1;
  border-top: 1px solid var(--border);
}

/*
 * The provider buttons.
 *
 * Wrapping rather than a fixed row: there are two today and a third is one
 * environment variable away, and three of them at this width is a row that
 * overflows the dialog rather than one that reflows.
 */
.server-providers {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.server-provider {
  flex: 1 1 140px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 500;
  cursor: pointer;
}

.server-provider:hover:not(:disabled) {
  background: var(--hover);
  border-color: var(--accent);
}

/* Disabled while the organisation is empty, and it has to *look* disabled —
   a control that refuses without saying so is the invisible refusal
   `CLAUDE.md` 41 is about. The reason is in the `title`. */
.server-provider:disabled {
  opacity: 0.5;
  cursor: default;
}

/*
 * "Forgot your password?" — a link, not a button, because it is a way out of
 * this form rather than a way through it. Drawn only when the server has said
 * it can send mail; see `server-panel.ts`.
 */
.server-link {
  align-self: flex-start;
  padding: 4px 0;
  border: none;
  background: none;
  color: var(--accent);
  font-size: 12px;
  cursor: pointer;
  text-align: left;
}

.server-link:hover {
  text-decoration: underline;
}

.server-primary {
  padding: 7px 14px;
  border: none;
  border-radius: 6px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
  cursor: pointer;
}

.server-primary:disabled {
  opacity: 0.5;
  cursor: default;
}

.server-plain {
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: transparent;
  color: var(--text);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.server-plain:hover {
  background: var(--hover);
}

.server-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}

.server-main {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.server-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
}

.server-meta {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* A project is a button, because clicking it loads its files — a row that looks
   like text and behaves like a control is one nobody clicks. */
.server-project {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

/*
 * While the dialog is working, its controls say so rather than ignoring the
 * press. `guard` discards a call made while something else is in flight, and a
 * row that looks pressable and does nothing is the worst of the two states —
 * see the comment in `server-panel.ts`.
 */
.server-project:disabled,
.server-plain:disabled {
  opacity: 0.5;
  cursor: default;
}

.server-project:hover {
  background: var(--hover);
}

.server-project.active {
  border-color: var(--border);
  background: var(--bg-sunken);
}

/* Indented under the project they belong to — the libraries dialog's reasoning:
   an unindented list reads as a second, unrelated one. */
.server-files {
  display: flex;
  flex-direction: column;
  padding-left: 14px;
}

.server-file {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 0;
}

.server-file-name {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
  color: var(--text-muted);
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.server-open {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* A colour under a token: the swatch keeps showing the value and stops being a
   control, matching `.number-input.is-bound` beside it. The cursor is the tell
   — a swatch that still looked clickable would be the same invitation the
   picker used to be. */
.color-row.is-bound .color-swatch.is-bound {
  cursor: default;
  border-color: transparent;
  box-shadow: 0 0 0 1px var(--border);
}

.color-row.is-bound .token-chip {
  flex: 1 1 auto;
  min-width: 0;
}

/* ---------------------------------------------------------------------------
   Comparing two versions

   The same sheet shape as the merge dialog, widened for two canvases. The
   canvases carry a chequerboard behind them for the same reason a colour
   swatch does: a design on a transparent page and one on a white page are
   different pictures, and a white pane would show both as white.
   --------------------------------------------------------------------------- */

.compare-sheet {
  position: fixed;
  z-index: 90;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(820px, 94vw);
  max-height: 86vh;
  display: flex;
  flex-direction: column;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.compare-sheet h3 {
  margin: 0 0 10px;
  font-size: 14px;
}

.compare-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.compare-panes {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}

.compare-pane {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.compare-picker {
  width: 100%;
  min-height: 26px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.compare-canvas {
  width: 100%;
  height: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: var(--checker-a);
  background-image:
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%),
    linear-gradient(45deg, var(--checker-b) 25%, transparent 25%, transparent 75%, var(--checker-b) 75%);
  background-size: 12px 12px;
  background-position: 0 0, 6px 6px;
}

.compare-caption {
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.compare-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}

.compare-count {
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--bg-sunken);
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.compare-list {
  width: 100%;
  margin-top: 8px;
}

.compare-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 0;
  border-top: 1px solid var(--border);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.compare-row-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The kind of change, colour-coded the way the layer list marks state: the
   word is the label, the colour is what makes a list of forty scannable. */
.compare-row-kind {
  flex: 0 0 auto;
  color: var(--text-muted);
}

.compare-row[data-change='added'] .compare-row-kind {
  color: var(--positive);
}

.compare-row[data-change='removed'] .compare-row-kind {
  color: var(--danger);
}

.compare-clean,
.compare-more,
.compare-empty {
  margin: 8px 0 0;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.5;
}

.compare-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 14px;
}

.compare-cancel,
.compare-restore {
  min-height: 28px;
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.compare-restore {
  border-color: var(--accent);
  background: var(--accent-fill);
  color: var(--on-accent);
}

.compare-restore:disabled {
  opacity: 0.5;
  cursor: default;
}

/* The page both panes show, when the file has more than one. Hidden entirely
   for a single-page file rather than shown disabled: a control with one choice
   in it is furniture. */
.compare-page-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.compare-page-label {
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.compare-page-picker {
  width: auto;
  min-width: 140px;
}

/* A change-list row is a button — clicking it frames that layer in both panes.
   The appearance is reset back to the row it replaced, because the affordance
   here is the hover and the pressed state, not a button's chrome. */
button.compare-row {
  width: 100%;
  border: 0;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: left;
  cursor: pointer;
}

button.compare-row:hover {
  background: var(--bg-sunken);
}

/* The framed layer. Its own colour, matching the outline drawn on the canvas,
   so the row and the box on the picture read as the same thing. */
button.compare-row.is-focused {
  background: var(--accent-soft);
}

button.compare-row.is-focused .compare-row-name {
  font-weight: 600;
}

/* ---------------------------------------------------------------------------
   Importing a design from another tool.

   A dialog rather than a panel, and for the same reason the server one is:
   an import is a decision taken once, it changes what the document contains,
   and it has nothing to say while somebody is drawing.
   --------------------------------------------------------------------------- */
.import-backdrop {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 64px 24px 24px;
  background: var(--overlay);
  overflow-y: auto;
}

.import-dialog {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 560px;
  max-height: min(720px, calc(100vh - 96px));
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.import-header {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 10px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}

.import-header h2 {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 16px;
}

.import-close {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.import-close:hover:not(:disabled) {
  background: var(--hover);
  color: var(--text);
}

.import-close:disabled {
  opacity: 0.4;
  cursor: default;
}

.import-body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 16px 20px 20px;
  overflow-y: auto;
}

/* Each source is its own block with a rule between, because the two ways in are
   genuinely different — one is a file and the other is an API — and running
   them together would read as one form with too many fields. */
.import-group + .import-group {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.import-group h3,
.import-report h3 {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.import-note {
  margin: 0 0 12px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
}

/* The "what is coming next" line. Quieter than the rest, because it is context
   rather than an instruction, and a person reading the screen to do something
   should reach the buttons before they reach the roadmap. */
.import-note-quiet {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.import-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
}

.import-field span {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.import-field input {
  min-width: 0;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.import-field input:focus {
  border-color: var(--accent);
  outline: none;
}

.import-primary {
  padding: 7px 14px;
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.import-primary:hover {
  filter: brightness(1.06);
}

.import-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
}

/* The headline number: how much of the file came across. It is the question
   somebody arrives at this screen with, so it is the largest thing on it. */
.import-score {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 12px;
}

.import-score strong {
  font-size: 32px;
  line-height: 1;
  color: var(--positive);
}

.import-score[data-status='completed-with-warnings'] strong {
  color: var(--text);
}

.import-score[data-status='failed'] strong {
  color: var(--danger);
}

.import-score span {
  font-size: 12px;
  color: var(--text-muted);
}

/* Grouped, not listed: two hundred identical warnings about layer blur are one
   fact, and two hundred lines are a list nobody reads. */
.import-list {
  margin: 0 0 12px;
  padding-left: 18px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text);
}

.import-list li[data-support='unavailable'] {
  color: var(--danger);
}

.import-list li[data-support='partial'],
.import-list li[data-support='converted'] {
  color: var(--text-muted);
}

.import-error {
  margin: 0 0 12px;
  padding: 10px 12px;
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  background: var(--danger-soft);
  font-size: 12px;
  line-height: 1.5;
  color: var(--text);
}

/* A link this browser cannot open.
 *
 * Full-screen and opaque on purpose. The first version of this was a line in
 * the status bar — correct, readable, and completely lost underneath the file
 * the editor had fallen back to, which is what a person reported as "a
 * completely different design opened". A refusal has to be the size of the
 * mistake it is describing. */
.link-refused {
  position: fixed;
  inset: 0;
  z-index: 130;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--bg-sunken);
  overflow-y: auto;
}

.link-refused-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-width: 460px;
  margin: auto;
  padding: 28px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.link-refused-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: var(--bg-sunken);
  color: var(--text-muted);
  font-size: 20px;
}

.link-refused-headline {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
}

.link-refused-detail {
  margin: 0;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  line-height: 1.5;
  color: var(--text-muted);
}

/* The address that was followed, selectable. Somebody asking a colleague
 * "does this open for you?" needs to be able to copy the link they were
 * sent, and by this point the address bar may be holding something else. */
.link-refused-url {
  width: 100%;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-sunken);
  color: var(--text-muted);
  font-family: var(--mono);
  font-size: 12px;
}

.link-refused-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

.link-refused-primary {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 14px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  font-weight: 600;
  cursor: pointer;
}

.link-refused-secondary {
  flex: 0 1 auto;
  min-width: 0;
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.link-refused-secondary:hover,
.link-refused-primary:hover {
  filter: brightness(1.06);
}

/* The access controls in the Share dialog.
 *
 * A role was a word in this list for as long as the list existed, while
 * `changePermission` and `denyAccess` sat unused on the service — so the only
 * way to change what somebody could do with a file was a terminal. */
.share-role {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.share-role-select {
  padding: 3px 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--panel);
  color: var(--text);
  font-size: 12px;
}

.share-role-fixed {
  color: var(--text-muted);
  font-size: 12px;
}

.share-remove {
  padding: 3px 8px;
  border: 1px solid transparent;
  border-radius: 5px;
  background: transparent;
  color: var(--text-muted);
  font-size: 12px;
  cursor: pointer;
}

.share-remove:hover:not(:disabled) {
  border-color: var(--border);
  color: var(--danger);
}

/* Colleagues who are not on the file yet. This platform sends no email, so
 * without these the only way to add somebody was to type an address that
 * nothing on any screen ever showed. */
.share-suggestions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 2px 0 4px;
}

.share-chip {
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text);
  font-size: 12px;
  cursor: pointer;
}

.share-chip:hover:not(:disabled) {
  border-color: var(--accent);
}

.share-chip:disabled {
  opacity: 0.6;
  cursor: default;
}

/* The token editor: aliases, a live preview, composite members.
 *
 * Every class here is load-bearing rather than decorative — an alias list with
 * no rules is a column of buttons over the form it was opened from, and a
 * preview with no rules is a line of text that reads like part of the value. */
.token-form-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 8px;
}

.token-form-label {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.token-value-field {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
}

.token-member-label {
  flex: 0 0 96px;
  /* `min-width: auto` on a flex item resolves to its min-content size and
   * silently beats `flex-basis` — the trap `stylesheet.test.ts` exists to
   * catch, and it caught this one. */
  min-width: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.token-value-field .token-form-input {
  flex: 1 1 auto;
  min-width: 0;
}

.token-alias-button {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--panel);
  color: var(--text-muted);
  cursor: pointer;
}

.token-alias-button:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.token-members {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Drawn inside the field rather than as a second popover: a nested popover
 * closes the form it was opened from, and the value being edited has to stay
 * on screen while a reference for it is chosen. */
.token-alias-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  max-height: 240px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.token-alias-search {
  flex: 0 0 auto;
  padding: 6px 8px;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  font-size: 12px;
}

.token-alias-rows {
  overflow-y: auto;
}

.token-alias-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 5px 8px;
  border: 0;
  background: transparent;
  color: var(--text);
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.token-alias-row:hover {
  background: var(--bg-sunken);
}

/* Offered, not hidden: a filter that hides a token somebody knows exists is a
 * filter they work around by typing the path by hand. */
.token-alias-row.is-other-type {
  opacity: 0.55;
}

.token-alias-path {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.token-alias-meta {
  flex: 0 0 auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.token-alias-empty {
  margin: 0;
  padding: 8px;
  color: var(--text-muted);
  font-size: 12px;
}

.token-preview {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 6px 0 2px;
  padding: 6px 8px;
  border-radius: 6px;
  background: var(--bg-sunken);
  font-size: 12px;
}

.token-preview.is-bad {
  color: var(--danger);
}

.token-preview-label {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.token-preview-value {
  font-family: var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.token-preview-error {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.token-preview-swatch {
  width: 14px;
  height: 14px;
  border: 1px solid var(--border);
  border-radius: 3px;
}

.token-references {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 8px 0;
}

.token-reference-group {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px;
}

.token-reference-title {
  margin-right: 4px;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.token-reference-link {
  padding: 2px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.token-reference-link:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.token-reference-more {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.token-form-duplicate {
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font-size: 12px;
  cursor: pointer;
}

/* Token branches.
 *
 * Flat, the list was a wall of dotted paths, and the operations that act on a
 * whole branch — rename, move, delete — had nowhere to live. */
.token-group {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  padding: 4px 6px;
  border-radius: 5px;
  background: var(--bg-sunken);
}

.token-group-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.token-group-count {
  flex: 0 0 auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.token-group-menu-button {
  flex: 0 0 auto;
}

.token-group-actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 220px;
}

.token-group-summary {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* The swatch that opens the colour picker on a colour token's value.
 *
 * Hollow when the field holds a reference: there is no single colour to show,
 * and a swatch painted with what the reference resolves to today would invite
 * somebody to replace a live reference with a frozen copy of it. */
.token-swatch-button {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: transparent;
  cursor: pointer;
}

.token-swatch-button.is-reference {
  background:
    linear-gradient(45deg, var(--border) 25%, transparent 25%, transparent 75%, var(--border) 75%),
    linear-gradient(45deg, var(--border) 25%, transparent 25%, transparent 75%, var(--border) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
}

/* Dragging a token onto a set. Pointer events rather than HTML5 drag-and-drop,
 * so these two classes are the only feedback there is — without them the
 * gesture is invisible and reads as nothing happening. */
.token-row.is-dragging {
  opacity: 0.5;
}

.set-row.is-drop-target {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The password on a share link — typed in the dialog that makes it, and typed
 * again by whoever follows it. Never read back in either place: the server
 * keeps a hash, so a field showing dots would imply a password that could be
 * recovered. */
.share-link-password {
  min-width: 0;
  padding: 3px 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--panel);
  color: var(--text);
  font-size: 12px;
}

.link-refused-password {
  width: 100%;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

/* The Share dialog's second screen. */
.share-row-drill {
  cursor: pointer;
}

.share-row-drill:hover {
  background: var(--bg-sunken);
}

.share-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.share-back:hover {
  background: var(--bg-sunken);
  color: var(--text);
}

/* The icon set has no left caret, so the right one is turned round rather than
 * a near-miss glyph being used — a fallback letter on a back button reads as a
 * stray character. */
.share-back .is-flipped,
.share-back.is-flipped {
  transform: scaleX(-1);
}

/* Reserved height for the layer rows a long list did not build. See
   `virtual-list.ts`: the panel renders only what the scroll container can
   show, and these two blocks carry the height of everything above and below
   it, so the scrollbar still describes the whole document. */
.layer-spacer {
  flex: 0 0 auto;
}

/* ------------------------------------------------------------- dev mode */

/* Two panels in one sidebar, and the mode decides which. A class on the root
   rather than JavaScript moving elements: `ViewState.apply` already sets it,
   and a stylesheet is the cheapest correct answer to "which of these two". */
#inspect-panel {
  display: none;
}

:root.dev-mode #inspector-panel {
  display: none;
}

:root.dev-mode #inspect-panel {
  display: block;
}

/* Dev Mode's dock used to be the design dock at 40% opacity with the pointer
   turned off — because every tool in it edits and Dev Mode does not, and
   dimming was the cheapest way to stop a row of controls that silently do
   nothing. It is a *different set* now (`devToolGroups()`): move, measure,
   annotate, comment, and the eyedropper where the platform has one. Nothing in
   it edits, so nothing in it needs disabling, and the four things a person
   actually does in this mode are one click away instead of unreachable. */

.inspect-row {
  display: flex;
  align-items: center;
  gap: 6px;
}

.inspect-row .field-value {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

.inspect-token {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--on-accent-soft);
  background: var(--accent-soft);
  border-radius: 4px;
  padding: 1px 5px;
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.inspect-copy {
  flex: 0 0 auto;
  opacity: 0;
}

.inspect-row:hover .inspect-copy,
.inspect-copy:focus-visible {
  opacity: 1;
}

.code-targets {
  display: flex;
  gap: 2px;
  padding: 0 8px 6px;
}

.code-target {
  border: 0;
  background: none;
  border-radius: 5px;
  padding: 3px 7px;
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-subtle);
  cursor: default;
}

.code-target:hover {
  background: var(--bg-sunken);
}

.code-target.is-active {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

.code-block {
  margin: 0 8px 8px;
  padding: 8px;
  border-radius: 6px;
  background: var(--bg-sunken);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 320px;
  overflow: auto;
  user-select: text;
}

.inspect-warning {
  margin: 0 8px 8px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--warning, var(--text-subtle));
}

/*
 * A note on a layer, in Dev Mode.
 *
 * A card rather than a line, and the reason is what a note now carries: a
 * category, its words, and the properties pinned under it. Three kinds of
 * thing on one baseline row is a row nobody can scan — the card gives each a
 * band, and the border is what says where one note ends and the next begins
 * when a layer has three.
 */
.inspect-note {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0 8px 8px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}

.inspect-note-top {
  display: flex;
  align-items: center;
  gap: 6px;
}

.inspect-note-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

/*
 * The category chip.
 *
 * `--chip` is set inline from the label's own hash (`categoryColour`), so the
 * colour is a property of the word rather than of a stored row — see
 * `annotations.ts`. The text is the token's, not the chip's: a filled chip at
 * this size fights the panel, and a tinted background with the same hue in the
 * text reads as one thing at a glance.
 */
.inspect-note-category {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  padding: 1px 6px;
  border: 0;
  border-radius: 999px;
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
  color: var(--chip, var(--text-muted));
  background: color-mix(in srgb, var(--chip, var(--text-muted)) 14%, transparent);
}

.inspect-note-category.is-empty {
  color: var(--text-muted);
  background: transparent;
  border: 1px dashed var(--border-strong);
}

.inspect-note-category-field,
.inspect-category-field {
  flex: 1 1 auto;
  min-width: 0;
  padding: 1px 6px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text);
  background: var(--bg);
}

.inspect-note-text {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  font-size: 12px;
  color: var(--text);
  background: var(--bg-sunken);
}

.inspect-note-text:focus-visible {
  border-color: var(--accent);
  outline: none;
}

/*
 * The pinned properties.
 *
 * A description list, because that is what it is: a key and the value read
 * live off the layer. The value is `is-verbatim` — it is the document's own
 * number and must not be translated — and `is-missing` is the property that
 * has since gone, which stays on screen saying so rather than disappearing.
 */
.inspect-note-props {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  gap: 2px 8px;
  margin: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.inspect-note-props dt {
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.inspect-note-props dd {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.inspect-note-props dd.is-missing {
  color: var(--text-muted);
  font-style: italic;
}

.inspect-note-unpin,
.inspect-note-remove,
.inspect-category-remove {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  opacity: 0;
}

.inspect-note:hover .inspect-note-unpin,
.inspect-note:hover .inspect-note-remove,
.inspect-category:hover .inspect-category-remove,
.inspect-note-unpin:focus-visible,
.inspect-note-remove:focus-visible,
.inspect-category-remove:focus-visible {
  opacity: 1;
}

.inspect-note-pin {
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--accent);
  cursor: pointer;
}

.inspect-note-empty {
  margin: 0 8px 8px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.inspect-categories {
  margin: 0 8px 8px;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-sunken);
}

.inspect-category {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.inspect-category-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.inspect-category-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text);
  cursor: text;
}

.inspect-category-count {
  flex: 0 0 auto;
  color: var(--text-muted);
}

/*
 * Its own names, not the design inspector's.
 *
 * `.inspector-name` there is the editable name *input*, and a rule written for
 * a read-only span would have reached it — two panels, one selector, and the
 * field people rename layers with silently restyled.
 */
.inspect-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.inspect-type {
  margin-left: auto;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-subtle);
}

/* ------------------------------------------------- ready for dev (update 102) */

/* A badge on the layer row, not a column: the fact is true of a handful of
   frames in a file of hundreds of layers, and a column would spend width on
   every row saying "no". */
.layer-ready {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  color: var(--positive);
  font-size: 12px;
  flex: 0 0 auto;
}

.inspect-ready {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  color: var(--positive);
  font-size: 12px;
  border-bottom: 1px solid var(--border);
}

/* The page a marked frame is on. A heading inside the list rather than a
   separate list per page, so the order on screen is the order in the file. */
.inspect-ready-page {
  padding: 8px 10px 2px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-subtle);
}

.inspect-ready-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 10px;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.inspect-ready-row:hover {
  background: var(--hover);
}

.inspect-ready-row > span:first-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.inspect-ready-when {
  margin-left: auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  flex: 0 0 auto;
}

/* The number beside "Ready for dev". Its own class rather than a reused badge:
   nothing else in this panel counts anything, and borrowing a class from a
   panel that does would tie two unrelated layouts together. */
.section-count {
  margin-left: auto;
  padding: 1px 6px;
  border-radius: 8px;
  background: var(--hover);
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* ------------------------------------------- computed properties (update 111) */

/* A formula row: the property on the left, the expression on the right. Its own
   class rather than `.field`, because the input has to be monospaced and has to
   be allowed to be long — an expression is read left to right, and a field that
   truncates it in the middle is a field you cannot check. */
.binding-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 6px 10px;
}

.binding-row > .field-label {
  flex: 0 0 72px;
  min-width: 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.binding-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 24px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--panel);
  color: var(--text);
  /* The stack directly, not `var(--font-mono, …)`: a custom property nobody
     declares is a fallback pretending to be a themeable value, and
     `theme.test.ts` has caught exactly that spelling before. */
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* Why an expression does not work, under the field it belongs to. Full width
   rather than beside the input: these are sentences ("there is nothing called
   props.inStok"), and a sentence in a 90px column is a sentence nobody reads. */
.binding-error {
  flex: 1 0 100%;
  margin: 4px 0 0 80px;
  color: var(--danger);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.4;
}

/* --------------------------------------------------- prototyping (update 109) */

/* Which of the three inspector panels is on screen. The pair of rules for Dev
   Mode further up says why this is a class on the root rather than JavaScript
   moving elements: `ViewState.apply` already sets it, and a stylesheet is the
   cheapest correct answer to "which of these three". */
#prototype-panel {
  display: none;
}

:root.prototype-mode #inspector-panel {
  display: none;
}

:root.prototype-mode #prototype-panel {
  display: block;
}

/* One interaction: the trigger, the action and whatever that action needs.
   Boxed, because a layer with four of them is otherwise four unlabelled runs
   of dropdowns and no way to see where one ends. */
.proto-reaction {
  margin: 8px 10px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-sunken);
}

.proto-reaction-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}

.proto-reaction-title {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 500;
}

.proto-field {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}

/* `min-width: 0` on the control, not only on the label: a flex item's
   `min-width: auto` resolves to its min-content size, which for a `<select>`
   with a long frame name in it is wider than the sidebar — and the panel would
   scroll sideways rather than the name being clipped. The same trap the fixed
   `flex-basis` rules further up reset, in a different control. */
.proto-field > .field-label {
  flex: 0 0 84px;
  min-width: 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.proto-field > select,
.proto-field > input {
  flex: 1 1 auto;
  min-width: 0;
}

/* A destination that is not there any more. Red on the label rather than on
   the control, so the row reads as "this one is wrong" without the dropdown
   itself looking disabled — it is still the thing you fix it with. */
.proto-field.is-broken > .field-label {
  color: var(--danger);
}

.proto-select,
.proto-number,
.proto-url,
.proto-state-name,
.proto-state-value,
.proto-expression,
.proto-sample,
.proto-flow-name {
  height: 24px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

.proto-number {
  font-variant-numeric: tabular-nums;
}

/* A variable the page remembers. The same box as an interaction, because it is
   the same kind of thing on the same panel — a named row with three controls
   under it — and two different boxes would say the two are unrelated. */
.proto-state {
  margin: 8px 10px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-sunken);
}

/* An expression is code, and it is read character by character: the `l`/`1`
   and `O`/`0` pairs in `state.total1` decide whether the formula works, and a
   proportional font makes them the same shape. The same call the binding field
   makes, with the same stack spelled out rather than a `--font-mono` token
   that does not exist. */
.proto-expression {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* The JSON a source answers with. A textarea rather than a one-line field: it
   is a document, it is read as much as it is typed, and a shape that has to be
   scrolled sideways one line at a time is a shape nobody checks. `height: auto`
   undoes the 24px the field rule above sets for its siblings. */
.proto-sample {
  height: auto;
  min-height: 64px;
  padding: 6px;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  line-height: 1.4;
  resize: vertical;
  white-space: pre;
}

/* A sample is taller than a row, so its label sits at the top of it rather than
   floating in the middle of a four-line box. */
.proto-field:has(> .proto-sample) {
  align-items: flex-start;
}

/* An address, and the path into what it answers. Monospaced for the reason an
   expression is: a URL is read character by character when it does not work,
   and `rn` against `m` in a proportional font is where the hour goes. */
.proto-url {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* Replace the sample with what the endpoint actually answers. Full width and
   under the fields it acts on, rather than an icon beside the address: it does
   something to the field below it, and an icon would have to be discovered. */
.proto-fetch {
  width: 100%;
  margin-top: 8px;
  height: 26px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.proto-fetch:hover:not(:disabled) {
  background: var(--hover);
}

/* Disabled rather than absent, because "sign in first" is something the person
   can act on and a missing button is a feature they conclude does not exist.
   The reason rides on `title`, which is the one place a disabled control can
   still carry one. */
.proto-fetch:disabled {
  opacity: 0.5;
  cursor: default;
}

/* The starting value of a true/false variable. Left-aligned with the other
   controls rather than stretched, because a checkbox that fills the column
   reads as a wide button. */
.proto-state-flag {
  flex: 0 0 auto;
  margin: 0;
}

/* Why an interaction's expression does not work. Full width under the row for
   the reason the binding error is: a sentence in an 84px column beside a field
   is a sentence nobody reads. */
.proto-error {
  margin: 6px 0 0;
  color: var(--danger);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.4;
}

/* The one line of guidance the smart-animate row needs: it pairs layers by
   name, and a renamed layer fades instead of moving — which looks like the
   feature being broken rather than like a rename. */
.proto-note {
  margin: 8px 0 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.4;
}

.proto-check {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.proto-flow-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 10px;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.proto-flow-row:hover {
  background: var(--hover);
}

.proto-flow-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.proto-flow-frame {
  margin-left: auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  flex: 0 0 auto;
}

.proto-broken-note {
  margin: 0;
  padding: 4px 10px 10px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.4;
}

/* ---------------------------------------------------------- the presentation

   Fixed and over everything, including the file browser: while a prototype is
   playing it is the whole application, and a panel showing through it would be
   a panel somebody clicks on the way to a button in the design. */
.presentation {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  flex-direction: column;
  /* Near-black in **both** themes, and a literal rather than a token on
     purpose — the same bargain the tool dock makes. What surrounds a design
     being presented is not part of the interface being themed: it is the wall
     the artwork hangs on, and a light one throws its own colour onto every
     screenshot taken of a prototype. */
  background: rgba(0, 0, 0, 0.94);
}

.present-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  /* White on that near-black wall, in both themes, for the same reason. */
  color: #ffffff;
  background: rgba(0, 0, 0, 0.35);
  font-size: 12px;
  flex: 0 0 auto;
}

.present-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

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

.present-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: inherit;
  cursor: pointer;
}

.present-button:hover {
  background: rgba(255, 255, 255, 0.12);
}

/* The stage is the positioning context for every canvas in it, and it has to
   be one: the frames are placed by `left`/`top` in *stage* pixels, and a
   missing `position` here would place them against the viewport instead —
   which is the same picture until the bar above changes height. */
.present-stage {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
}

.present-frames,
.present-overlays {
  position: absolute;
  inset: 0;
}

/* The overlay *layer* spans the whole stage and is above the frame, so without
   this it takes every press in the presentation — including the ones aimed at
   the screen underneath, and including the ones aimed at nothing when no
   overlay is open at all. The canvases inside it take the pointer back, so a
   press on a dialog still lands on the dialog. Found in Chromium: the probe
   asks what `elementFromPoint` returns in the middle of the stage, and it
   answered with this element. */
.present-overlays {
  pointer-events: none;
}

.present-overlay-canvas {
  pointer-events: auto;
}

.present-canvas,
.present-overlay-canvas {
  position: absolute;
  /* No transition property: the animation is driven frame by frame from
     `requestAnimationFrame`, so a CSS transition here would be a second
     animation fighting the first — and the two disagree about where the frame
     is for as long as they overlap. */
  will-change: transform, opacity;
}

.present-overlay-canvas {
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}

/* Dimming behind an overlay. Its own element rather than a fill drawn into the
   canvas, because it covers the *whole* stage while the frame canvas covers
   only the frame, and because its opacity is animated by the same mechanism
   as everything else here. */
.present-backdrop {
  position: absolute;
  inset: 0;
  background: #000000;
  opacity: 0;
  /* Never takes the pointer, whether or not an overlay is open: the stage's own
     handler already decides what a press outside an overlay means, and a
     backdrop that swallowed the event would leave that decision to an element
     that has no idea what is open. */
  pointer-events: none;
}

/* The hotspot flash. `pointer-events: none` is load-bearing: this canvas
   covers the whole stage, and without it every press in a presentation would
   land on the hint layer and nothing would ever be clicked. */
.present-hints {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* What a presentation says when it refuses. The editor's status bar is behind
   this screen, so a message sent there would be invisible — update 28's lesson,
   applied rather than repeated. */
.present-note {
  position: absolute;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  padding: 8px 14px;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.72);
  color: #ffffff;
  font-size: 12px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease-out;
}

.present-note.is-visible {
  opacity: 1;
}

/*
 * The repeater's own sentence, under its fields.
 *
 * Three states and three colours, because they are three different pieces of
 * news: a count ("6 of 40, from Card") is ordinary, a hint is something to do
 * next, and a refusal is something that is wrong. Rendering all three in the
 * muted grey a note usually gets would leave a frame that draws nothing
 * looking exactly like a frame that is working — which is the whole question
 * somebody is asking when they read this line.
 */
.repeat-note {
  margin: 8px 0 0;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.4;
  word-break: break-word;
}

.repeat-note.is-hint {
  color: var(--text);
}

.repeat-note.is-error {
  color: var(--danger);
}

/*
 * A server field that states a value instead of asking for one.
 *
 * Readable and selectable, and visibly not a place to type: the address is the
 * origin this page was served from and there is nothing to decide about it
 * (bug 380). Without the different ground it reads as an ordinary field that
 * has stopped working.
 */
.server-field.is-fixed input {
  background: var(--bg-sunken);
  color: var(--text-muted);
  cursor: default;
}

/* ---------------------------------------------------------------------------
   Selection colours.

   Two shapes for one section: a strip of swatches when it is collapsed, a list
   of rows when it is open. Figma's arrangement, and the reason is that the
   question has two forms — "what is this made of", answered by looking, and
   "change that one everywhere", answered by reading.
   --------------------------------------------------------------------------- */
.sel-colours-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
}

/* A swatch is a control, not a picture: pressing one opens the picker on that
   colour. So it gets a focus ring and a pointer, and the checkerboard behind it
   is what makes a 20%-alpha colour distinguishable from a pale one. */
.sel-colour-chip {
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  min-width: 0;
  padding: 0;
  border: 1px solid var(--swatch-ring);
  border-radius: 4px;
  cursor: pointer;
  background-color: var(--swatch-color, #000);
  background-image: linear-gradient(
      var(--swatch-color, #000) 0 100%
    ),
    conic-gradient(var(--checker-a) 0 25%, var(--checker-b) 0 50%, var(--checker-a) 0 75%, var(--checker-b) 0);
  background-size:
    100% 100%,
    8px 8px;
  opacity: 1;
}

/* The alpha is expressed by letting the checkerboard through, which is only
   possible on the top layer — hence the two backgrounds above rather than one
   colour and a border. */
.sel-colour-chip {
  background-blend-mode: normal;
}

.sel-colour-chip:hover {
  border-color: var(--accent);
}

.sel-colour-chip[aria-expanded='true'] {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* `+68`. A control rather than a caption: it is the fastest way into the list,
   and the number is the reason somebody wants the list. */
.sel-colours-more {
  height: 20px;
  padding: 0 6px;
  border: 1px solid var(--field-border);
  border-radius: 4px;
  background: var(--field);
  color: var(--text-muted);
  font: inherit;
  font-size: 10px;
  cursor: pointer;
}

.sel-colours-more:hover {
  background: var(--field-hover);
  border-color: var(--border);
  color: var(--text);
}

.sel-colour-row {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 26px;
}

.sel-colour-row-swatch {
  flex: 0 0 20px;
  min-width: 0;
}

/* The name is the row's biggest target and it is a button: on a token row it
   opens the token list, on a plain one the colour picker. Styled as text rather
   than as a control on purpose — the swatch beside it already says "press me",
   and two buttons that look like buttons in a 26px row is a row nobody can
   read. */
.sel-colour-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text);
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}

.sel-colour-name:hover {
  text-decoration: underline;
}

/* A colour that comes from a token is a different kind of thing, and the whole
   row acts differently: pressing it chooses another *token* rather than another
   colour. The accent is what says so before the press. */
.sel-colour-row.is-bound .sel-colour-name {
  color: var(--accent);
}

/* The two buttons every row carries: select everything wearing this colour, and
   put a style or a token on it. Small and quiet — they sit between a name that
   can be long and a count that must stay visible. */
.sel-colour-focus,
.sel-colour-tokens {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
}

.sel-colour-row .alpha-input {
  flex: 0 0 40px;
  width: 40px;
  min-width: 0;
}

.sel-colour-count {
  flex: 0 0 auto;
  color: var(--text-subtle);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}

/* A sentence under a control saying something the control cannot: why a list is
   a button, why a list may be short. */
.panel-note {
  margin: 4px 0 0;
  color: var(--text-subtle);
  font-size: 10px;
  line-height: 1.4;
}

/* ---------------------------------------------------------------------------
   Component properties, shaped like Figma's.

   `◇ Property 1 · Default, Variant2` — a glyph for the kind, the name, the
   values, then the actions. It was three side-by-side inputs, which put a
   comma-separated list of variant values in a 264px panel and let somebody
   delete a value that variants were still sitting on.
   --------------------------------------------------------------------------- */
.property-row {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 4px;
  border-radius: 5px;
  background: var(--field);
}

.property-row:hover {
  background: var(--field-hover);
}

.property-name {
  flex: 0 1 auto;
  min-width: 0;
  width: 84px;
  height: 22px;
  padding: 0 4px;
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.property-name:hover,
.property-name:focus {
  border-color: var(--border);
  background: var(--bg);
  outline: none;
}

/* The values, after the middot. Truncated rather than wrapped: the row is one
   line tall and a second line here would push every row below it down by the
   length of somebody's variant names. */
.property-values {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.property-values::before {
  content: '· ';
}

.property-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
}

/* Only under the pointer or when it is saying something. A row of five icons
   per property, times eight properties, is forty icons competing with the names
   they belong to. */
.property-actions > button:not(.is-warning) {
  opacity: 0;
}

.property-row:hover .property-actions > button,
.property-actions > button:focus-visible {
  opacity: 1;
}

.property-actions .is-warning {
  color: var(--warning);
}

/* How many layers this property drives. A number rather than "3 layers": the
   row has room for one, and the word is in the tooltip. */
.property-targets {
  min-width: 18px;
  height: 20px;
  padding: 0 4px;
  border: 1px solid var(--field-border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text-muted);
  font: inherit;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.property-targets:hover {
  border-color: var(--accent);
  color: var(--text);
}

/* The two popovers: the property editor and the component configuration. */
.property-editor,
.component-config {
  width: 260px;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.property-editor h4,
.component-config h4 {
  margin: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-weight: 600;
  color: var(--text);
}

.property-values-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.property-value-row {
  display: flex;
  align-items: center;
  gap: 4px;
}

.property-value-row input {
  flex: 1 1 auto;
  min-width: 0;
  height: 24px;
  padding: 0 6px;
  border: 1px solid var(--field-border);
  border-radius: 4px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

/* ---------------------------------------------------------------------------
   Sticky notes.

   Six colours as swatches, because a palette that is a picker turns a board
   into a gradient nobody can group by — and grouping by colour is what the
   colours are for.
   --------------------------------------------------------------------------- */
.sticky-colours {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.sticky-colour {
  width: 26px;
  height: 26px;
  flex: 0 0 26px;
  min-width: 0;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 6px;
  /* The colour comes from the element: these six are *document* colours, not
     theme ones, so they live in `whiteboard.ts` beside the ink they are paired
     with — a palette token here would be a second copy that drifts. The
     fallback is the same shape as the swatch's own. */
  background: var(--swatch-color, var(--field));
  cursor: pointer;
  box-shadow: inset 0 0 0 1px var(--swatch-ring);
}

.sticky-colour:hover {
  border-color: var(--border-strong);
}

/* The one it is wearing. An outline rather than a tick: at 26px a tick over a
   pale yellow is invisible, and the question being answered — "which of these
   is on" — is answered faster by a ring than by reading six squares. */
.sticky-colour.is-on {
  border-color: var(--accent);
}

/* The board mode's own class. It does not make the canvas read-only and does
   not change what a tool does; it exists so the chrome can say which mode it is
   in without every control asking. */
.whiteboard-mode .mode-tab[aria-selected='true'] {
  color: var(--text);
}

/* ---------------------------------------------------------------------------
   Tasks.

   A list and a board, in a 248px rail. The rail rather than a full window
   because a task here is *about a layer*, and clicking one has to take you to
   it — which means the canvas must still be on screen. A full-window board
   would be a second application with a back button.
   --------------------------------------------------------------------------- */
.tasks-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.tasks-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
}

.tasks-views {
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: 6px;
  background: var(--field);
}

.tasks-view {
  height: 20px;
  padding: 0 8px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  cursor: pointer;
}

.tasks-view.is-on {
  background: var(--bg);
  color: var(--text);
}

.tasks-add {
  margin-left: auto;
  width: 22px;
  height: 22px;
  min-width: 0;
  padding: 0;
  border: 1px solid var(--field-border);
  border-radius: 5px;
  background: var(--field);
  color: var(--text);
  cursor: pointer;
}

.tasks-add:disabled {
  opacity: 0.4;
  cursor: default;
}

.tasks-count {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-variant-numeric: tabular-nums;
}

.tasks-empty {
  padding: 16px 12px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.tasks-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Five columns scroll sideways rather than shrinking. Five columns in 248px is
   five columns of nothing. */
.tasks-timeline {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 8px 12px;
}

/*
 * Today, as a hairline the whole strip long.
 *
 * Absolute against the scroller rather than drawn per row: one line is one
 * line, and a segment per row is a dotted line that stops at the last task.
 * `--at` is written by the panel, because only it knows the window.
 */
.timeline-today {
  position: absolute;
  top: 4px;
  bottom: 4px;
  /* Spanning exactly the track column: 8px of padding + the 84px name column +
     the 6px gap on the left, and the same arithmetic for the 56px date column
     on the right. The line itself is the pseudo-element, placed at `--at`
     inside that box — which is the only way to say "this percentage of the
     track" in CSS, since a percentage cannot be multiplied by a percentage. */
  left: 98px;
  right: 70px;
  pointer-events: none;
}

.timeline-today::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--at, 0%);
  width: 1px;
  background: var(--accent);
  opacity: 0.55;
}

.timeline-row {
  display: grid;
  grid-template-columns: 84px 1fr 56px;
  align-items: center;
  gap: 6px;
  padding: 3px 4px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.timeline-row:hover {
  background: var(--field-hover);
}

.timeline-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.timeline-track {
  position: relative;
  height: 10px;
  border-radius: 5px;
  background: var(--field);
}

.timeline-bar {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--from, 0%);
  width: var(--width, 2%);
  border-radius: 5px;
  background: var(--accent);
}

.timeline-row[data-status='done'] .timeline-bar {
  background: var(--text-muted);
}

/*
 * Late is a ring rather than a different fill, for the reason the task pin's
 * lateness is: a colour swap says "this is a different kind of thing", and a
 * ring says "this one, and it is still what it was".
 */
.timeline-row.is-late .timeline-bar {
  outline: 2px solid var(--danger);
  outline-offset: 1px;
}

.timeline-when {
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  text-align: right;
  white-space: nowrap;
}

.tasks-board {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  display: flex;
  gap: 8px;
  padding: 8px;
}

.tasks-column {
  flex: 0 0 168px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.tasks-column-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 4px 4px;
  color: var(--text-muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.tasks-column-count {
  font-variant-numeric: tabular-nums;
}

.task-row,
.task-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  background: var(--field);
}

.task-row:hover,
.task-card:hover {
  background: var(--field-hover);
  border-color: var(--border);
}

/* Selected, so the panel and the canvas agree about what is being looked at. */
.task-row.is-selected,
.task-card.is-selected {
  border-color: var(--accent);
}

/* Late is a left edge, not a red card: a task can be late *and* high priority,
   and a fill can only say one thing. */
.task-row.is-late,
.task-card.is-late {
  box-shadow: inset 3px 0 0 var(--danger);
}

.task-row.is-high .task-title,
.task-card.is-high .task-title {
  font-weight: 600;
}

.task-title {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.task-title:hover {
  color: var(--accent);
}

.task-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  color: var(--text-subtle);
  font-size: 10px;
}

.task-where {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 110px;
}

.task-who,
.task-tag {
  padding: 0 5px;
  border-radius: 999px;
  background: var(--hover);
  color: var(--text-muted);
}

.task-due.is-late {
  color: var(--danger);
}

.task-actions {
  display: flex;
  align-items: center;
  gap: 4px;
}

.task-status {
  flex: 1 1 auto;
  min-width: 0;
  height: 22px;
  border: 1px solid var(--field-border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.task-details {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
}

.task-field {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 10px;
  color: var(--text-muted);
}

.task-field input,
.task-field select,
.task-field textarea {
  height: auto;
  min-height: 24px;
  min-width: 0;
  padding: 3px 6px;
  border: 1px solid var(--field-border);
  border-radius: 4px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  resize: vertical;
}

/* The `⋯` on a file card. Under the pointer, and for the keyboard always —
   a control visible only on hover is one nobody tabbing can find. */
.fb-card-more {
  margin-left: auto;
  opacity: 0;
}

.fb-card:hover .fb-card-more,
.fb-card-more:focus-visible,
.fb-card-more[aria-expanded='true'] {
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   The workshop strip: a clock and a round of voting, over the canvas.

   Large and centred at the top, because both are things a *room* watches on a
   projector while somebody talks — not properties you go to a panel to change.
   Only on screen when something is running: a strip that always says
   "0:00 · no voting" is one people learn to ignore before the first workshop.
   --------------------------------------------------------------------------- */
.workshop-bar {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

/* Tabular numerals, or the clock jitters sideways once a second — which is the
   one thing a clock in front of a room must not do. */
.workshop-clock {
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  min-width: 62px;
  text-align: center;
}

.workshop-clock.is-soon {
  color: var(--warning);
}

.workshop-clock.is-over {
  font-size: 14px;
  color: var(--danger);
}

.workshop-votes,
.workshop-result {
  font-size: 12px;
  color: var(--text-muted);
  border: 0;
  background: transparent;
  font-family: inherit;
  padding: 0;
}

.workshop-result {
  cursor: pointer;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.workshop-result:hover {
  color: var(--accent);
}

.workshop-action {
  width: 26px;
  height: 26px;
  min-width: 0;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--field-border);
  border-radius: 999px;
  background: var(--field);
  color: var(--text);
  cursor: pointer;
}

.workshop-action:hover {
  background: var(--field-hover);
  border-color: var(--border);
}

/* The motion timeline.

   A strip under the canvas rather than a panel in a rail, because time is
   horizontal — and part of the canvas *column* rather than floating over the
   artwork, because a playhead somebody is dragging must not sit on top of the
   thing they are watching move. The strip is `hidden` until something on the
   page has a keyframe on it, so a file nobody animates carries no strip. */
.canvas-column {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

/* The section carries the height and the drag handle; the strip inside it
   scrolls. Splitting the two is what keeps the handle alive across the panel's
   `replaceChildren` — the same arrangement the pages list uses. */
.timeline-section {
  position: relative;
  flex: 0 0 var(--timeline-height, 220px);
  height: var(--timeline-height, 220px);
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.timeline-host {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: 168px 1fr;
  grid-template-areas:
    'transport ruler'
    'names rows';
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--panel);
  border-top: 1px solid var(--border);
}

.timeline-transport {
  grid-area: transport;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 8px;
  height: 20px;
  border-bottom: 1px solid var(--border);
  border-right: 1px solid var(--border);
}

/**
 * What the strip says in animation mode before anything is keyed.
 *
 * Spanning both columns of the second row: there are no track names to sit
 * beside, so the sentence gets the width of the strip rather than the 1fr
 * column the rows canvas normally has.
 */
.timeline-empty {
  grid-column: 1 / -1;
  grid-row: 2;
  margin: 0;
  padding: 10px 12px;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
}

.timeline-play,
.timeline-pause {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
}

/* Two bars, drawn rather than named: the icon set here is addressed by
   codepoint and this repository has no `pause` codepoint it can verify, so a
   guessed one would be silently the wrong picture. */
.timeline-pause-mark {
  display: inline-flex;
  gap: 2px;
  align-items: center;
}

.timeline-pause-mark i {
  display: block;
  width: 3px;
  height: 10px;
  background: currentColor;
}

.timeline-clock {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  white-space: nowrap;
}

/* The duration, typed. `2000 ms` beside the clock, the way Figma's transport
   carries it — the command that opened a prompt is still there for the
   keyboard, and this is the one on screen (bug 495). */
.timeline-duration {
  width: 52px;
  height: 20px;
  padding: 0 4px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-small);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.timeline-unit {
  font-size: var(--type-body-small);
  color: var(--text-muted);
}

/* The value the property has at the playhead, at the end of its track row. */
.timeline-track-value {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.timeline-key {
  color: var(--accent);
}

.timeline-loop {
  margin-left: auto;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  cursor: pointer;
}

.timeline-loop:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

.timeline-names {
  grid-area: names;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border);
}

.timeline-track-name {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 22px;
  padding: 0 8px;
  background: none;
  border: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text);
  cursor: pointer;
  overflow: hidden;
  white-space: nowrap;
  text-align: left;
}

.timeline-track-name:hover {
  background: var(--hover);
}

.timeline-track-name.is-selected {
  background: var(--accent-soft);
}

.timeline-track-property {
  color: var(--text-muted);
  margin-left: auto;
  flex: 0 0 auto;
}

.timeline-canvas {
  grid-area: rows;
  display: block;
  width: 100%;
  touch-action: none;
}

/* A menu row that explains itself.

   Two words cannot tell somebody which of two kinds of page they want, and a
   row you have to hover to read is a row people conclude does nothing
   (lesson 22). Only where the sentence earns its line — a menu whose every item
   explains itself is a menu nobody reads at all. */
.context-menu-item.has-note {
  /* `height: auto` is the load-bearing line. An ordinary row is a fixed 26px
     with `overflow: hidden` on its label, so a second line is laid out and then
     clipped — the sentence is in the DOM, measures as an element, and is not on
     screen. The probe caught it by asking `elementFromPoint` at the note's own
     centre and getting the *title* back. */
  height: auto;
  align-items: flex-start;
  padding-top: 6px;
  padding-bottom: 6px;
}

.context-menu-item.has-note .context-menu-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  white-space: normal;
}

.context-menu-note {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.35;
  color: var(--text-muted);
  max-width: 230px;
}

/* The templates button, on a riff page. Beside the Actions button in the dock
   and styled with it — it is the same kind of thing: not a tool. */
.dock-templates {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--dock-muted);
  font-size: var(--type-body-large);
  letter-spacing: var(--type-body-large-tracking);
  cursor: pointer;
}

.dock-templates:hover {
  background: var(--dock-hover);
  color: var(--dock-text);
}

/* The kind of page a row is, in the page list. A riff's icon is the board's, so
   the two kinds are told apart at a glance rather than by opening them. */
.page-row[data-kind='riff'] .page-open .icon {
  color: var(--accent);
}

/* The plan: every task in the file, in columns a team arranges.

   Over the canvas rather than beside it — a plan page has no artwork, so the
   canvas under it has nothing to draw, and covering it keeps the dock, the
   rulers and both rails exactly where they are. */
.plan-host {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px 64px;
  overflow: auto;
  overscroll-behavior: contain;
  background: var(--bg-sunken);
}

.plan-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
}

.plan-count {
  font-size: 12px;
  color: var(--text-muted);
}

.plan-swimlane {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
}

.plan-swimlane:hover {
  border-color: var(--border-strong);
}

.plan-add-column {
  margin-left: auto;
}

/* Work that is in the file and not on this board. A banner rather than a log
   line: "3 tasks are not on this board" is a sentence somebody can act on. */
.plan-unmapped {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  padding: 7px 10px;
  border-radius: 8px;
  background: var(--warning-soft);
  color: var(--warning);
  font-size: 12px;
}

.plan-lane {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  margin-top: 6px;
  font-size: var(--type-body-medium);
  font-weight: 600;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.plan-lane-count {
  font-weight: 400;
  color: var(--text-muted);
}

.plan-columns {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  flex: 0 0 auto;
  /* Columns keep their width and the strip scrolls: five columns squeezed into
     the space available is five columns of nothing. */
  overflow-x: auto;
  padding-bottom: 6px;
}

.plan-column {
  flex: 0 0 268px;
  /* `min-width: auto` on a flex item resolves to its min-content size, which
     for a column holding a long task title is wider than the basis — so the
     column silently grows and the strip stops being five equal columns. The
     same trap the inspector's fields have, and `stylesheet.test.ts` is what
     found it here. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px;
  border-radius: 10px;
  background: var(--panel);
  border: 1px solid var(--border);
}

.plan-column.is-over {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}

.plan-column.is-over-limit .plan-column-count {
  color: var(--warning);
  font-weight: 600;
}

.plan-column-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  font-weight: 600;
}

.plan-column-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.plan-column-count {
  flex: 0 0 auto;
  font-weight: 400;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.plan-column-menu {
  flex: 0 0 auto;
}

.plan-column-note {
  margin: 0;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  line-height: 1.4;
  color: var(--text-muted);
}

.plan-cards {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 28px;
}

.plan-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  border-radius: 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  cursor: grab;
}

.plan-card.is-dragging {
  opacity: 0.5;
}

.plan-card.is-high {
  border-left: 3px solid var(--danger);
}

.plan-card.is-late .plan-card-due {
  color: var(--danger);
}

.plan-card-title {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 12px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.plan-card-title:hover {
  text-decoration: underline;
}

.plan-card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.plan-card-where,
.plan-card-who,
.plan-card-due {
  white-space: nowrap;
}

.plan-empty {
  max-width: 420px;
  font-size: 12px;
  color: var(--text-muted);
}

/* The library's board: every task in every file.

   Its own body rather than a mode of the grid — the filters above the grid all
   ask questions about *files*, and drawing them over a board would be four
   controls that do nothing. */
.fb-board {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 4px 24px 24px;
}

.fb-board-columns {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  overflow-x: auto;
  padding-bottom: 8px;
}

.fb-board-column {
  flex: 0 0 250px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px;
  border-radius: 10px;
  background: var(--panel);
  border: 1px solid var(--border);
}

.fb-board-column-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
  font-weight: 600;
}

.fb-board-column-count {
  font-weight: 400;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.fb-board-card {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 8px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.fb-board-card:hover {
  border-color: var(--border-strong);
}

.fb-board-card.is-high {
  border-left: 3px solid var(--danger);
}

.fb-board-card-title {
  font-size: 12px;
  color: var(--text);
}

.fb-board-card-file,
.fb-board-card-who {
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.fb-board-empty {
  max-width: 460px;
  padding: 12px 24px;
  font-size: 12px;
  color: var(--text-muted);
}

/* Which properties an instance holds its own value of, one row each.

   A list rather than a mark on every field above: a field knows its *label*,
   and a label-to-key table kept in step by hand is the thing that goes stale
   silently. These keys are the model's. */
.override-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
}

.override-where {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text);
  cursor: pointer;
}

.override-where:hover:not(:disabled) {
  text-decoration: underline;
}

.override-where:disabled {
  color: var(--text-subtle);
  cursor: default;
}

.override-what {
  flex: 0 0 auto;
  font-size: var(--type-body-medium);
  letter-spacing: var(--type-body-medium-tracking);
  color: var(--text-muted);
}

.override-reset {
  flex: 0 0 auto;
}

/* Recordings — queue item H5's player.
   Its own dialog rather than a section of the libraries one: a recording is
   about a meeting, not about a design system, and a rail with one unrelated
   tab is how a dialog becomes a drawer. */
.recordings-backdrop {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 64px 24px 24px;
  background: var(--overlay);
  overflow-y: auto;
}

.recordings-dialog {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 640px;
  max-height: min(720px, calc(100vh - 96px));
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.recordings-header {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 10px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}

.recordings-close {
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  cursor: pointer;
}

.recordings-close:hover {
  background: var(--hover);
  color: var(--text);
}

.recordings-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 14px;
  min-height: 0;
  padding: 16px 20px 20px;
  overflow-y: auto;
}

.recordings-note {
  display: flex;
  flex-direction: column;
  gap: 3px;
  color: var(--text-muted);
  font-size: var(--type-body-medium);
  line-height: var(--type-body-medium-line);
}

.recordings-note strong {
  color: var(--text);
}

.recordings-note p {
  margin: 0;
}

.recordings-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.recording-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 10px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  text-align: left;
  cursor: pointer;
}

.recording-row:hover {
  background: var(--hover);
}

.recording-row.active {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.recording-meta {
  color: var(--text-muted);
  font-size: var(--type-body-small);
}

.recording-detail {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.recording-length {
  color: var(--text-muted);
  font-size: var(--type-body-small);
}

.recording-audio {
  width: 100%;
}

.recordings-primary {
  align-self: flex-start;
  height: 28px;
  padding: 0 12px;
  border: 0;
  border-radius: 6px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font: inherit;
  font-size: var(--type-body-medium);
  cursor: pointer;
}

.recording-moments {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* A moment is a row you press to move the playhead, so it reads as a list of
   times rather than as a paragraph: the offset in a fixed column, the place it
   goes beside it. */
.recording-moment {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 5px 8px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  text-align: left;
  cursor: pointer;
}

.recording-moment:hover {
  background: var(--hover);
}

.recording-at {
  flex: 0 0 44px;
  min-width: 0;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.recording-what {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A decision on a layer — queue item H7. Three lines stacked, because a
   decision is a sentence rather than a value: laid out in the inspector's
   label/field grid it would land in the 90px label column and wrap to six
   lines, which is the same failure `repeat-note` was given a rule for. */
.decision-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
}

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

.decision-text {
  color: var(--text);
  font-size: var(--type-body-medium);
  line-height: var(--type-body-medium-line);
  white-space: normal;
}

.decision-who {
  color: var(--text-muted);
  font-size: var(--type-body-small);
}

/* Aligned to the start rather than stretched: a full-width Remove under every
   decision reads as the section's own action rather than as that row's. */
.decision-remove {
  align-self: flex-start;
  margin-top: 2px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-small);
  cursor: pointer;
}

.decision-remove:hover {
  color: var(--danger);
}

/* A decision among the moments: the same row, marked. The accent is on the
   left edge rather than on the text, because the two lists are read as one
   column and a coloured sentence beside a plain one reads as a state (unread,
   selected) rather than as a kind. */
.recording-moment.is-decision {
  border-left: 2px solid var(--accent);
  border-radius: 0 6px 6px 0;
  padding-left: 6px;
}

/* The export row in the recordings dialog. A row of quiet buttons rather than
   one primary each: three formats are three answers to one question, and three
   filled buttons in a row is a dialog shouting. */
.recordings-actions {
  display: flex;
  gap: 8px;
}

.recordings-plain {
  height: 26px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-medium);
  cursor: pointer;
}

.recordings-plain:hover {
  background: var(--hover);
}

/* The curve control on the timeline's transport row. A select rather than four
   buttons: the strip is twenty pixels tall and already carries the play button,
   the clock and the loop mode — four spelled-out curve names across it is a row
   that overflows, and an overflowing row here put the loop button on top of
   them, where a press lands on the wrong control. */
.timeline-curve {
  height: 16px;
  max-width: 120px;
  padding: 0 2px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-small);
}

.timeline-curve:hover {
  color: var(--text);
}

/* ---------------------------------------------------------------------------
   The mobile viewer — queue items G1 and G4.

   A stylesheet rather than a second layout, because what a phone gets is *this*
   editor with most of it absent: the canvas, the comment pins, the page
   navigation and the export are already built, and a second shell reading the
   same document would be a second place for each of them to be slightly wrong.

   Absent, not dimmed. Dev Mode dims the tool dock on purpose — a toolbar that
   changes shape between modes moves everything under the pointer — and that
   argument is about a person switching modes on one screen. Nobody switches
   into the viewer on a phone: they arrive in it, and a greyed panel taking a
   third of a 390px screen is a third of the screen spent on saying no.
   --------------------------------------------------------------------------- */
:root.view-mode .sidebar,
:root.view-mode .tool-dock,
:root.view-mode #timeline-section,
:root.view-mode .mode-tabs,
:root.view-mode .meeting-strip,
:root.view-mode .presence-group,
:root.view-mode .share-button {
  display: none;
}

/* The canvas takes the whole window, minus the two bars. */
:root.view-mode .body {
  grid-template-columns: 1fr;
}

/* Toolbar: the file's name and nothing that edits. Wrapping is deliberate —
   a long file name on a 390px screen is two lines, and two lines is better
   than a name cut off at "Marketing site — desk…". */
:root.view-mode .toolbar {
  height: auto;
  min-height: 40px;
  flex-wrap: wrap;
  gap: 4px;
  padding: 4px 8px;
}

/* The bottom bar: pages, comments, export. Three, and the queue says three —
   "и всё". A fourth would be the first step back towards the editor. */
.phone-bar {
  display: none;
}

:root.view-mode .phone-bar {
  display: flex;
  /* Above the sheet and its backdrop, so the button that opened a sheet is the
     button that closes it. The probe found the other order: with the sheet over
     the bar the only way out was to choose something, and with the backdrop
     over the bar there was no way out at all. */
  position: relative;
  z-index: 121;
  align-items: stretch;
  gap: 4px;
  padding: 4px 8px;
  /* Above the safe area on a phone with a home indicator, and zero everywhere
     else: `env()` falls back to its second argument where it is unsupported. */
  padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border);
  background: var(--panel);
}

/* 44px, which is the smallest target a finger hits reliably — the number both
   Apple and Google publish, and the reason these are not the 22px buttons the
   rest of this interface uses. */
.phone-bar-button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--type-body-small);
  cursor: pointer;
}

.phone-bar-button:hover,
.phone-bar-button.is-open {
  background: var(--hover);
  color: var(--text);
}

/* A sheet from the bottom, not a popover at the edge: a popover anchored to a
   control near the screen edge is a popover half off the screen, and the
   keyboard covers the bottom half of a phone anyway. */
.phone-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 120;
  max-height: 60vh;
  overflow-y: auto;
  padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border);
  border-radius: 12px 12px 0 0;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.phone-sheet-title {
  padding: 4px 8px 8px;
  color: var(--text-muted);
  font-size: var(--type-body-small);
}

.phone-sheet-row {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 0 12px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--type-body-large);
  text-align: left;
  cursor: pointer;
}

.phone-sheet-row.is-current,
.phone-sheet-row:hover {
  background: var(--hover);
}

/* Behind the sheet and over everything else, so a tap anywhere closes it —
   which is what every other dialog here does and what a person tries first.
   Transparent rather than dimmed: the sheet takes at most 60% of the screen
   and the canvas behind it is what somebody is looking at. */
.phone-sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 119;
}

/* The comment thread on a phone — queue item G5.
   A sheet along the bottom rather than a 268px popover anchored to a pin: a
   popover near the screen edge is half off it, and the on-screen keyboard takes
   the bottom half of the screen the moment anybody types a reply. Along the
   bottom edge the keyboard pushes it up instead of covering it. */
:root.view-mode .comment-popover {
  position: fixed;
  left: 8px;
  right: 8px;
  bottom: 8px;
  top: auto;
  width: auto;
  max-width: none;
  max-height: 50vh;
  z-index: 122;
}

/* --- Variables view --------------------------------------------------------

   Figma's/Pixso's variables table, as a full-screen overlay (a rail is 280px
   and a column-per-mode table is wider than that). The backdrop is the dim over
   the editor; the panel is the card; the body is the rail plus the table. */
.variables-view {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.45);
  padding: 32px;
}

.variables-panel {
  display: flex;
  flex-direction: column;
  width: min(1200px, 96vw);
  height: min(820px, 92vh);
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-panel);
  overflow: hidden;
  outline: none;
}

.variables-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
}

.variables-title {
  margin: 0;
  font-size: var(--type-heading-small);
  font-weight: 600;
}

.variables-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-subtle);
  cursor: pointer;
}

.variables-close:hover {
  background: var(--hover);
  color: var(--text);
}

.variables-body {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}

.variables-empty {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: var(--text-muted);
  text-align: center;
  padding: 24px;
}

.variables-empty-hint {
  max-width: 420px;
  font-size: var(--type-body-medium);
  color: var(--text-subtle);
}

.variables-rail {
  flex: 0 0 240px;
  /* The flexbox trap: without this, `min-width: auto` is the rail's min-content
     width, which a long collection name pushes past 240px. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border-right: 1px solid var(--border);
  overflow-y: auto;
}

.variables-filter,
.variables-search {
  width: 100%;
  box-sizing: border-box;
  height: 30px;
  padding: 0 8px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font-size: var(--type-body-medium);
}

.variables-collections {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 4px;
}

.variables-collection {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 9px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font-size: var(--type-body-large);
  text-align: left;
  cursor: pointer;
}

.variables-collection:hover {
  background: var(--hover);
}

.variables-collection.is-active {
  background: var(--accent-soft);
  color: var(--on-accent-soft);
}

.variables-collection-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.variables-collection-count {
  flex: 0 0 auto;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
}

.variables-main {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: auto;
  padding: 12px 14px;
}

.variables-summary {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  margin-bottom: 8px;
}

.variables-none {
  color: var(--text-muted);
  font-size: var(--type-body-large);
  padding: 12px 2px;
}

/* The table is a grid: a wide name column and one flexible column per mode,
   driven by --mode-count so the header and the rows line up exactly. */
.variables-table {
  display: flex;
  flex-direction: column;
  min-width: max-content;
}

.variables-row {
  display: grid;
  grid-template-columns: minmax(200px, 1.4fr) repeat(var(--mode-count, 1), minmax(140px, 1fr));
  align-items: center;
  gap: 0;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  width: 100%;
}

.variables-row:hover:not(.variables-row-head) {
  background: var(--hover);
}

.variables-row-head {
  position: sticky;
  top: 0;
  background: var(--panel);
  cursor: default;
  z-index: 1;
}

.variables-row-head .variables-cell {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.variables-group {
  padding: 10px 10px 4px;
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  font-weight: 600;
}

.variables-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  min-width: 0;
  font-size: var(--type-body-large);
  overflow: hidden;
}

.variables-cell-name {
  gap: 8px;
}

.variables-cell-value.is-missing {
  color: var(--text-subtle);
}

.variables-name,
.variables-value,
.variables-cell-mode {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.variables-type-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--text-subtle);
}

.variables-swatch {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  border-radius: 4px;
  border: 1px solid var(--border-strong);
}

/* The editable pieces of a Variables cell. The name is a button that reads as
   text; the value edits in place; a boolean is a checkbox. */
.variables-name {
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  padding: 0;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.variables-name:hover {
  text-decoration: underline;
}

.variables-cell-value {
  cursor: text;
}

.variables-edit {
  width: 100%;
  box-sizing: border-box;
  height: 22px;
  padding: 0 6px;
  border: 1px solid var(--accent);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
}

.variables-bool {
  cursor: pointer;
}

/* The Details panel — the right rail for the selected variable. */
.variables-row.is-selected {
  background: var(--accent-soft);
}

.variables-details {
  flex: 0 0 260px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 14px;
  border-left: 1px solid var(--border);
  overflow-y: auto;
}

.variables-details-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.variables-details-title {
  margin: 0;
  font-size: var(--type-body-large);
  font-weight: 600;
}

.variables-details-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text-subtle);
  cursor: pointer;
}

.variables-details-close:hover {
  background: var(--hover);
  color: var(--text);
}

.variables-details-field,
.variables-details-section {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.variables-details-label,
.variables-details-heading {
  color: var(--text-subtle);
  font-size: var(--type-body-small);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.variables-details-name,
.variables-details-desc {
  width: 100%;
  box-sizing: border-box;
  padding: 6px 8px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  resize: vertical;
}

.variables-details-values {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.variables-details-value {
  display: grid;
  grid-template-columns: 80px 1fr;
  align-items: center;
  gap: 8px;
}

.variables-details-mode {
  color: var(--text-subtle);
  font-size: var(--type-body-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.variables-details-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--type-body-large);
  cursor: pointer;
}

.variables-details-actions {
  display: flex;
  gap: 8px;
  margin-top: auto;
  padding-top: 8px;
}

.variables-details-action {
  flex: 1 1 auto;
  padding: 7px 10px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.variables-details-action:hover {
  background: var(--hover);
}

.variables-details-action.is-danger:hover {
  border-color: var(--danger);
  color: var(--danger);
}

.variables-details-scopes {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.variables-details-hint {
  margin: 0 0 2px;
  color: var(--text-subtle);
  font-size: var(--type-body-small);
}

/* Creating from the Variables view — toolbar, the New variable menu, and the
   rail's Create collection / header Add mode buttons. */
.variables-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.variables-new-wrap {
  position: relative;
}

.variables-new,
.variables-create-collection {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 10px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.variables-new:hover,
.variables-create-collection:hover {
  background: var(--hover);
}

.variables-create-collection {
  margin-top: 8px;
  width: 100%;
  justify-content: center;
}

.variables-new-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  min-width: 140px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  box-shadow: var(--shadow-panel);
}

.variables-new-item {
  padding: 7px 9px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.variables-new-item:hover {
  background: var(--hover);
}

.variables-add-mode {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text-subtle);
  cursor: pointer;
}

.variables-add-mode:hover {
  background: var(--hover);
  color: var(--text);
}

/* An alias cell — one variable pointing at another, drawn as a chip. */
.variables-alias {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 6px 1px 4px;
  border-radius: 4px;
  background: var(--accent-soft);
  color: var(--on-accent-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* Rename/remove controls for collections and modes. The remove buttons are
   quiet until the row (or header cell) is hovered, so they do not clutter. */
.variables-collection-remove,
.variables-mode-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--text-subtle);
  cursor: pointer;
  opacity: 0;
}

.variables-collection:hover .variables-collection-remove,
.variables-cell-mode:hover .variables-mode-remove {
  opacity: 1;
}

.variables-collection-remove:hover,
.variables-mode-remove:hover {
  background: var(--hover);
  color: var(--danger);
}

.variables-mode-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.variables-cell-mode {
  justify-content: space-between;
  gap: 4px;
}
