/* The Rails design system: components (docs/design/DESIGN-SYSTEM.md section 3).
   Every component exists once and is not restyled at a call site. Controls are capsules;
   containers take a role radius. Every colour, radius, size and duration is a token from
   tokens.css; the few lengths that are a control's own geometry are named at the top of its
   rule. Glass containers (.bar, .composer, .sheet, .menu, .toast) take their material from
   glass.css by adding .glass; the rules here give them only their shape and layout. */

/* ------------------------------------------------------------------
   Button: capsule, 44 high, Headline label, 8 gap to an icon, 20 padding. One primary per screen.
   ------------------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--button-gap);
  height: var(--space-touch); padding: 0 var(--button-padding-x); border: 0; border-radius: var(--radius-pill);
  font-size: var(--type-headline-rem); line-height: var(--type-headline-line); font-weight: var(--type-headline-weight);
  cursor: pointer; text-decoration: none; white-space: nowrap; color: inherit; background: none;
  transition: background var(--fade-fast), color var(--fade-fast), scale var(--spring-quick), opacity var(--fade-fast);
}
.btn:hover { text-decoration: none; }
.btn:active { scale: var(--button-press-scale); }
.btn--primary { background: var(--accent); color: var(--on-accent); }
.btn--secondary { background: var(--fill-2); color: var(--ink); }
.btn--secondary:active { background: var(--fill-3); }
/* Quiet is ink, not accent: a screen has one tint, and it belongs to the primary button or the
   agent's face (2.1). What marks a quiet button as a control is its Headline weight and its hover. */
.btn--quiet { background: transparent; color: var(--ink); }
.btn--quiet:hover:not([disabled], [aria-disabled="true"]) { background: var(--fill-1); }
.btn--quiet:active { background: var(--fill-2); }
.btn--destructive { background: var(--danger-fill); color: var(--on-accent); }
.btn--small { height: var(--button-small-height); padding: 0 var(--space-4); font-size: var(--type-subhead-rem); line-height: var(--type-subhead-line); }
.btn--large { height: var(--button-large-height); padding: 0 var(--space-6); }
.btn--icon { width: var(--space-touch); padding: 0; }
.btn--icon.btn--small { width: var(--button-small-height); }
/* A quiet icon button keeps its full hit target, but its hover, press and on states are a plate that
   hugs the glyph: a rounded square (radius-small) clipped to the content box, not a capsule around
   the whole target. The focus ring follows the plate. Filled icon buttons stay capsules. */
.btn--quiet.btn--icon { --icon-plate: var(--button-icon-plate); --icon-plate-inset: calc((var(--space-touch) - var(--icon-plate)) / 2);
  padding: var(--icon-plate-inset); background-clip: content-box; border-radius: calc(var(--radius-small) + var(--icon-plate-inset)); }
.btn--quiet.btn--icon.btn--small { --icon-plate: var(--button-icon-plate-small); --icon-plate-inset: calc((var(--button-small-height) - var(--icon-plate)) / 2); }
.btn--quiet.btn--icon:focus-visible { outline-offset: calc(var(--space-focus-ring) - var(--icon-plate-inset)); }
.btn--block { display: flex; width: 100%; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: var(--button-disabled-opacity); cursor: default; }
.btn > svg { flex: none; }
.glass .btn--secondary { background: var(--fill-2); color: var(--ink); }

/* ------------------------------------------------------------------
   Segmented control and switch: monochrome ink, so a bar never carries a second tint.
   ------------------------------------------------------------------ */
.seg {
  display: inline-flex; padding: var(--seg-padding); border-radius: var(--radius-pill); background: var(--fill-2); gap: var(--seg-gap);
}
.seg > button {
  height: var(--seg-height); padding: 0 var(--space-3); border: 0; border-radius: var(--radius-pill); background: transparent;
  color: inherit; font-size: var(--type-subhead-rem); line-height: var(--type-subhead-line); font-weight: var(--type-headline-weight); cursor: pointer;
  transition: background var(--spring-quick), box-shadow var(--spring-quick);
}
.seg > button[aria-pressed="true"], .seg > button[aria-checked="true"] { background: var(--raised); color: var(--ink); box-shadow: var(--shadow-1); }
/* A segmented control with a thumb (segmented.tsx sets data-thumb, --seg-thumb-x and --seg-thumb-w from the
   chosen segment) glides one raised thumb to the choice on spring-toggle instead of swapping fills. The
   thumb's place is a transform; its width only changes when the segments differ. */
.seg[data-thumb] { position: relative; isolation: isolate; }
.seg[data-thumb]::before { content: ""; position: absolute; z-index: -1; top: var(--seg-padding); bottom: var(--seg-padding); left: 0; width: var(--seg-thumb-w);
  translate: var(--seg-thumb-x) 0; border-radius: var(--radius-pill); background: var(--raised); box-shadow: var(--shadow-1);
  transition: translate var(--spring-toggle), width var(--spring-toggle); }
.seg[data-thumb] > button[aria-pressed="true"], .seg[data-thumb] > button[aria-checked="true"] { background: transparent; box-shadow: none; }
.seg[data-thumb-instant]::before { transition: none; }
[data-motion="reduce"] .seg[data-thumb]::before { transition: none; }
@media (prefers-reduced-motion: reduce) { :root:not([data-motion="full"]) .seg[data-thumb]::before { transition: none; } }

.switch {
  appearance: none; -webkit-appearance: none; margin: 0; width: var(--switch-width); height: var(--switch-height); border-radius: var(--radius-pill);
  background: var(--fill-3); position: relative; cursor: pointer; flex: none; transition: background var(--spring-quick);
}
.switch::after {
  content: ""; position: absolute; top: var(--switch-padding); left: var(--switch-padding); width: var(--switch-knob); height: var(--switch-knob); border-radius: 50%;
  background: var(--raised); box-shadow: var(--shadow-2); transition: translate var(--spring-quick);
}
.switch:checked { background: var(--ink); }
.switch:checked::after { translate: calc(var(--switch-width) - var(--switch-knob) - 2 * var(--switch-padding)) 0; background: var(--ground); }
.switch[disabled] { opacity: var(--button-disabled-opacity); cursor: default; }

/* ------------------------------------------------------------------
   Text field: capsule, 44 high, raised, a control border, Body text, muted placeholder.
   ------------------------------------------------------------------ */
.field {
  display: block; width: 100%; height: var(--space-touch); padding: 0 var(--space-4); border-radius: var(--radius-pill);
  border: var(--field-border) solid var(--control); background: var(--raised); color: var(--ink); font: inherit; outline: none;
  transition: box-shadow var(--fade-fast), border-color var(--fade-fast);
}
.field::placeholder { color: var(--muted); }
.field:focus-visible { outline: none; box-shadow: 0 0 0 var(--space-focus-ring) var(--ground), 0 0 0 calc(2 * var(--space-focus-ring)) var(--focus); }
.field[aria-invalid="true"], .field.is-invalid { border-color: var(--danger); }
.field[disabled] { opacity: var(--button-disabled-opacity); }
textarea.field { height: auto; min-height: var(--space-touch); padding-block: calc((var(--space-touch) - var(--type-body-line)) / 2); resize: none; border-radius: var(--radius-message); }
/* A select is the same capsule with the system's own caret drawn from tokens, never the browser's chrome. */
select.field {
  appearance: none; -webkit-appearance: none; padding-right: var(--space-8); text-overflow: ellipsis; cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: right calc(var(--space-4) + var(--space-1)) center, right var(--space-4) center;
  background-size: var(--space-1) var(--space-1); background-repeat: no-repeat;
}
/* A field with a prefix inside its capsule ("+1", "$", "rails.so/a/"): the capsule is the wrapper
   and the input inside it is bare, so the prefix can be any width and the ring stays the capsule's. */
.field--prefixed { display: flex; align-items: center; padding-right: 0; }
.field--prefixed:focus-within { box-shadow: 0 0 0 var(--space-focus-ring) var(--ground), 0 0 0 calc(2 * var(--space-focus-ring)) var(--focus); }
.field--prefixed:has(> input[aria-invalid="true"]) { border-color: var(--danger); }
.field--prefixed:has(> input:disabled) { opacity: var(--button-disabled-opacity); }
.field--prefixed > .field-prefix { flex: none; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.field--prefixed > input { flex: 1; min-width: 0; height: 100%; border: 0; background: none; outline: none; padding: 0 var(--space-4) 0 var(--space-1); color: var(--ink); font: inherit; }
.field--prefixed > input::placeholder { color: var(--muted); }
/* A labelled field: the label is always visible above the control, aligned with the capsule's text;
   the note or the error sits beneath. A placeholder is an example, never the label. */
.field-group { display: grid; gap: var(--space-2); }
.field-group > .field-note, .field-group > .field-error { margin-top: 0; }
.field-label { display: block; padding-inline: var(--space-4); font-size: var(--type-subhead-rem); line-height: var(--type-subhead-line); font-weight: var(--type-headline-weight); color: var(--ink); }
.field-error { display: block; margin-top: var(--space-2); padding-inline: var(--space-4); color: var(--danger); font-size: var(--type-footnote-rem); line-height: var(--type-footnote-line); }
.field-note { display: block; margin-top: var(--space-2); padding-inline: var(--space-4); color: var(--muted); font-size: var(--type-footnote-rem); line-height: var(--type-footnote-line); }

/* ------------------------------------------------------------------
   Pill and badge: capsule, 22 high, Footnote Semibold, tabular figures.
   ------------------------------------------------------------------ */
.pill, .badge {
  display: inline-flex; align-items: center; height: var(--pill-height); padding: var(--pill-padding-y) var(--pill-padding-x); border-radius: var(--radius-pill);
  font-size: var(--type-footnote-rem); line-height: var(--type-footnote-line); font-weight: var(--type-badge-weight);
  background: var(--fill-2); color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.badge { color: var(--muted); }
/* A badge's tone colours the word and its leading icon; the fill stays fill-2. */
.badge > svg { flex: none; width: var(--type-footnote-size); height: var(--type-footnote-size); margin-right: var(--space-1); }
.badge--accent { color: var(--accent-ink); }
.badge--success { color: var(--success); }
.badge--warning { color: var(--warning); }
.badge--danger { color: var(--danger); }
/* A pill that is a choice (a version, a filter) and is the chosen one. */
.pill.is-selected { background: var(--ink); color: var(--ground); }
.pill--free { background: none; color: var(--muted); padding: 0; }
.pill--added { background: none; color: var(--success); padding: 0; }
.glass .badge, .glass .pill { background: none; color: var(--ink); }

/* ------------------------------------------------------------------
   Card, row and list: flat, raised on surface, hairlines, no shadow at rest.
   ------------------------------------------------------------------ */
.card { background: var(--raised); border: var(--space-hairline) solid var(--hairline); border-radius: var(--radius-panel); padding: var(--card-padding); color: var(--ink); }
@media (min-width: 720px) { .card { padding: var(--card-padding-wide); } }
.card--interactive { display: block; width: 100%; text-align: left; cursor: pointer; text-decoration: none; transition: box-shadow var(--spring-quick), border-color var(--spring-quick); }
.card--interactive:hover { box-shadow: var(--shadow-1); border-color: var(--control); text-decoration: none; }
.card--interactive:active { background: var(--fill-1); }

.list { background: var(--raised); border: var(--space-hairline) solid var(--hairline); border-radius: var(--radius-panel); overflow: hidden; color: var(--ink); }
.row {
  display: flex; align-items: center; gap: var(--space-4); min-height: var(--space-row); padding: var(--row-padding-y) var(--row-padding-x);
  width: 100%; border: 0; background: none; color: inherit; text-align: left; text-decoration: none; position: relative;
}
/* The separator is drawn inside the row, inset from the group's leading edge, so a hover or a
   selected fill still reaches both edges. .list-item wraps a row (or a control such as a switch)
   that needs list semantics of its own; it carries the separator the same way. */
.list-item { display: block; position: relative; margin: 0; padding: 0; list-style: none; }
.row + .row::before, .list-item + .list-item::before {
  content: ""; position: absolute; top: 0; right: 0; left: var(--row-separator-inset);
  border-top: var(--space-hairline) solid var(--hairline); pointer-events: none;
}
/* A list or a card placed on glass is that glass's own material: fill-1 with no border, never a second pane. */
.glass .list, .glass .card { background: var(--fill-1); border-color: transparent; }
.glass .row + .row::before, .glass .list-item + .list-item::before { border-top-color: var(--fill-2); }
.row .grow { flex: 1; min-width: 0; }
.row .detail { color: var(--muted); }
.row .value { color: var(--ink); }
.row .chev { color: var(--muted); flex: none; }
a.row, button.row { cursor: pointer; transition: background var(--fade-fast); }
a.row:hover, button.row:hover { background: var(--fill-1); text-decoration: none; }   /* only interactive rows hover */
a.row:active, button.row:active { background: var(--fill-2); }
.row.is-selected { background: var(--accent-soft); }
.list-title { padding: var(--section-header-above) var(--section-header-inset) var(--section-header-below); font-size: var(--type-headline-rem); line-height: var(--type-headline-line); font-weight: var(--type-headline-weight); }

/* ------------------------------------------------------------------
   Disclosure: <details>. The summary is a 44-high row in Headline with the system's chevron at its
   end, which turns when it opens; never the browser's triangle.
   ------------------------------------------------------------------ */
details > summary { display: flex; align-items: center; gap: var(--space-2); min-height: var(--space-touch); list-style: none; cursor: pointer; font-weight: var(--type-headline-weight); }
details > summary::-webkit-details-marker { display: none; }
details > summary::after { content: ""; flex: none; width: var(--space-2); height: var(--space-2); margin-left: auto; margin-right: var(--space-1); border-right: var(--space-focus-ring) solid var(--muted); border-bottom: var(--space-focus-ring) solid var(--muted); rotate: -45deg; transition: rotate var(--spring-quick); }
details[open] > summary::after { rotate: 45deg; }

/* ------------------------------------------------------------------
   Spinner: indeterminate progress in currentColor, alone with a word or inside a busy button.
   An <svg class="spinner"> ring (a faint circle and a quarter arc), or an empty
   <span class="spinner">, which draws the same ring with a border. It keeps turning under
   Reduce Motion, because a stopped spinner reads as stalled; its turn is derived from the slow
   fade, so there is no literal duration.
   ------------------------------------------------------------------ */
.spinner { flex: none; animation: rails-spin calc(var(--fade-slow-duration) * 2.5) linear infinite; }
span.spinner { display: inline-block; width: var(--space-4); height: var(--space-4); border: var(--space-focus-ring) solid var(--fill-3); border-top-color: currentColor; border-radius: 50%; vertical-align: middle; }
.spinner-labelled { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); color: var(--muted); font-size: var(--type-subhead-rem); line-height: var(--type-subhead-line); }
@keyframes rails-spin { to { rotate: 360deg; } }

/* ------------------------------------------------------------------
   Notice: inline and flat. The tone is the leading icon's colour only.
   ------------------------------------------------------------------ */
.notice {
  display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--notice-padding-y) var(--notice-padding-x);
  border-radius: var(--radius-panel); background: var(--fill-1); color: var(--ink);
  font-size: var(--type-subhead-rem); line-height: var(--type-subhead-line);
}
.notice > svg, .notice > .notice-icon { flex: none; color: var(--muted); }
.notice > .grow { flex: 1; min-width: 0; }
.notice--warning > svg, .notice--warning > .notice-icon { color: var(--warning); }
.notice--danger > svg, .notice--danger > .notice-icon { color: var(--danger); }
.notice--success > svg, .notice--success > .notice-icon { color: var(--success); }
.notice > .btn--icon { margin: calc(-1 * (var(--space-touch) - var(--type-subhead-line)) / 2) calc(-1 * var(--space-2)); }

/* ------------------------------------------------------------------
   Glass containers: the bar, the composer, the sheet, the menu, the toast. Add .glass.
   ------------------------------------------------------------------ */
.bar {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: var(--space-bar); padding: 0 var(--space-1) 0 var(--space-3); border-radius: calc(var(--space-bar) / 2); --r: calc(var(--space-bar) / 2);
}
.bar .title, .bar > .grow { flex: 1; min-width: 0; }
.bar .title { font-size: var(--type-headline-rem); line-height: var(--type-headline-line); font-weight: var(--type-headline-weight); }
.bar .status { font-size: var(--type-footnote-rem); line-height: var(--type-footnote-line); color: var(--ink); }

.composer {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: var(--composer-height); padding: 0 var(--composer-inset); border-radius: calc(var(--composer-height) / 2); --r: calc(var(--composer-height) / 2);
  transition: border-radius var(--spring-quick);
}
.composer--multiline { border-radius: var(--radius-message); --r: var(--radius-message); align-items: flex-end; }
.composer .placeholder { flex: 1; min-width: 0; font-weight: var(--type-headline-weight); opacity: .9; }
.composer > textarea, .composer > .input { flex: 1; min-width: 0; border: 0; background: none; outline: none; resize: none; color: var(--ink);
  padding: calc((var(--composer-height) - var(--type-body-line)) / 2) var(--space-2); line-height: var(--type-body-line); max-height: calc(var(--composer-max-lines) * var(--type-body-line) + var(--composer-height) - var(--type-body-line)); }
.composer > textarea::placeholder { color: var(--ink); opacity: .9; font-weight: var(--type-headline-weight); }
/* The capsule is the field, so it wears the focus ring for its textarea, and only when the
   keyboard brought the focus there (prefs.js mirrors that as data-input="keyboard"). A text field
   always matches :focus-visible, so without this the composer would wear an accent ring at rest
   on every screen that focuses it on load, and read as a tinted composer. The caret is the focus
   indicator for a pointer. A composer built on another element (the app's is the morph's glass)
   opts in with data-composer="line" or "multiline". */
:is(.composer, [data-composer]) textarea:focus-visible { outline: none; }
:root[data-input="keyboard"] :is(.composer, [data-composer]):has(textarea:focus-visible) { outline: var(--space-focus-ring) solid var(--focus); outline-offset: var(--space-focus-ring); }
/* Past its first line the text scrolls inside the capsule. It keeps an inset from the top and
   bottom edges and fades into them, so a scrolled line is never cut flush against the rim. */
:is(.composer--multiline, [data-composer="multiline"]) textarea {
  margin-block: var(--composer-inset); padding-block: calc((var(--composer-height) - var(--type-body-line)) / 2 - var(--composer-inset));
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--composer-scroll-fade), #000 calc(100% - var(--composer-scroll-fade)), transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 var(--composer-scroll-fade), #000 calc(100% - var(--composer-scroll-fade)), transparent);
}
.composer .send { width: var(--composer-send); height: var(--composer-send); padding: 0; border-radius: 50%; }
.composer .send--ready { background: var(--accent); color: var(--on-accent); }

dialog.sheet:not([open]) { display: none; }   /* .sheet's own display must not show a closed <dialog> */
.sheet { border-radius: var(--radius-sheet); padding: var(--sheet-padding); --r: var(--radius-sheet); display: grid; gap: var(--space-3); }
.grabber { width: var(--sheet-grabber-width); height: var(--sheet-grabber-height); border-radius: var(--radius-pill); background: var(--fill-3); margin: 0 auto var(--space-1); }
.sheet .rows { display: grid; }
.sheet .rows .r { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); min-height: var(--space-touch); }
.sheet .rows .r + .r { border-top: var(--space-hairline) solid var(--fill-2); }
.sheet .actions { display: flex; gap: var(--space-2); }
.sheet .actions .btn { flex: 1; }
/* On a phone the sheet's actions stack, the one that goes forward on top, as the system's sheets do. */
@media (max-width: 767px) { .sheet .actions { flex-direction: column-reverse; } .sheet .actions .btn { flex: none; width: 100%; } }
/* A sheet's own padding is its inset: a section header inside it starts where the sheet's title does. */
.sheet .list-title { padding-inline: 0; }
.scrim { position: fixed; inset: 0; background: var(--dim); }

.toast { display: flex; align-items: center; gap: var(--space-2); border-radius: var(--radius-panel); padding: var(--toast-padding); --r: var(--radius-panel); font-size: var(--type-subhead-rem); line-height: var(--type-subhead-line); }
.toast > svg { flex: none; }
/* Where a toast goes: the top of the screen under the bar, centred, one at a time, arriving with
   spring-standard (a fade under Reduce Motion) and leaving after --toast-hold. A page whose bar
   is inset sets --toast-top; a toast inside a positioned shell sets position and --toast-z. */
.toast--placed {
  position: fixed; top: var(--toast-top, calc(var(--space-bar) + var(--space-3))); left: 50%; translate: -50% 0; z-index: var(--toast-z, 50);
  width: max-content; max-width: min(calc(100% - 2 * var(--space-4)), var(--space-measure));
  animation: rails-toast-in var(--spring-standard) both;
}
/* A page with no glass (Studio) shows the toast flat: raised, a hairline, shadow-2. */
.toast--flat { background: var(--raised); border: var(--space-hairline) solid var(--hairline); box-shadow: var(--shadow-2); color: var(--ink); }
@keyframes rails-toast-in { from { opacity: 0; scale: var(--glass-materialize-scale); } }

/* ------------------------------------------------------------------
   State dot: where a chat or request stands, one space-2 mark leading its row, on every row, always.
   idle      a hollow ring: nothing is happening and nothing waits
   working   solid, breathing: the agent is on it
   queued    a ring, breathing: it will start when something frees up
   needs     the attention amber: it waits on the person
   done      the accent: finished with something unread
   failed    danger: it stopped with an error
   The breathing is opacity, so the compositor runs it however long the work takes. The hit area is
   space-5 so its tooltip (state.tsx) is easy to reach.
   ------------------------------------------------------------------ */
.state-dot { display: inline-grid; place-items: center; flex: none; width: var(--space-5); height: var(--space-5); color: var(--muted); }
.state-dot::before { content: ""; box-sizing: border-box; width: var(--space-2); height: var(--space-2); border-radius: 50%;
  border: calc(1.5 * var(--space-hairline)) solid currentColor; background: transparent; }
.state-dot[data-state="idle"] { color: color-mix(in oklch, var(--muted) 70%, transparent); }
.state-dot[data-state="working"] { color: var(--ink); }
.state-dot[data-state="working"]::before { background: currentColor; opacity: .7; }
.state-dot:is([data-state="working"], [data-state="queued"])::before { animation: rails-breathe var(--breathe-duration) ease-in-out infinite; }
.state-dot[data-state="queued"] { color: var(--ink); }
.state-dot[data-state="needs"] { color: var(--attention); }
.state-dot[data-state="done"] { color: var(--accent-ink); }
.state-dot[data-state="failed"] { color: var(--danger); }
.state-dot:is([data-state="needs"], [data-state="done"], [data-state="failed"])::before { background: currentColor; }
@keyframes rails-breathe { 50% { opacity: .3; } }
[data-motion="reduce"] .state-dot::before { animation: none; }
@media (prefers-reduced-motion: reduce) { :root:not([data-motion="full"]) .state-dot::before { animation: none; } }

/* ------------------------------------------------------------------
   Fade text: a line that doesn't fit fades out over its last space-6 instead of ending in "…". While its
   row (.fade-host) is hovered or focused it glides right to left at a steady pace after tooltip-delay,
   fading at both ends so the rest can be read, and glides back when the pointer leaves. fade-text.tsx
   measures the overflow into --fade-shift and the glide's length into --fade-glide; the movement is a
   transform, so the compositor runs it. At rest only the trailing end fades; the leading fade comes with the
   movement.
   ------------------------------------------------------------------ */
@property --fade-start { syntax: "<length>"; inherits: false; initial-value: 0; }
.fade-text { display: block; min-width: 0; overflow: hidden; white-space: nowrap; }
.fade-text > span { display: inline-block; transition: translate var(--spring-quick); }
.fade-text[data-overflow] { --fade-start: 0; transition: --fade-start var(--spring-quick);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--space-6)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--space-6)), transparent); }
/* The leading fade only exists while the text is moving: it starts with the glide (after tooltip-delay) and
   grows over the glide's first space-6 of travel (--fade-lead, from fade-text.tsx), so it arrives exactly as
   the first letters slide under it; leaving, it shrinks as the text glides home. */
:is(.fade-host:hover, .fade-host:focus-visible) .fade-text[data-overflow] { --fade-start: var(--space-6); transition: --fade-start var(--fade-lead) linear var(--tooltip-delay); }
:is(.fade-host:hover, .fade-host:focus-visible) .fade-text[data-overflow] > span { translate: var(--fade-shift) 0; transition: translate var(--fade-glide) linear var(--tooltip-delay); }
[data-motion="reduce"] :is(.fade-host:hover, .fade-host:focus-visible) .fade-text[data-overflow] > span { translate: none; }
@media (prefers-reduced-motion: reduce) { :root:not([data-motion="full"]) :is(.fade-host:hover, .fade-host:focus-visible) .fade-text[data-overflow] > span { translate: none; } }

/* ------------------------------------------------------------------
   Scroll edges: a scroller (.scroll-fade) fades its content out over space-6 at an edge that has more
   beyond it, top and bottom, the same fade as a line of text, so nothing is cut off hard by its box. At
   an end, or when nothing overflows, that edge is sharp. scroll-fade.ts sets data-more-above and
   data-more-below; each edge's fade eases in and out on fade-standard.
   ------------------------------------------------------------------ */
@property --fade-above { syntax: "<length>"; inherits: false; initial-value: 0; }
@property --fade-below { syntax: "<length>"; inherits: false; initial-value: 0; }
.scroll-fade { --fade-above: 0; --fade-below: 0; transition: --fade-above var(--fade-standard), --fade-below var(--fade-standard);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-above), #000 calc(100% - var(--fade-below)), transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-above), #000 calc(100% - var(--fade-below)), transparent); }
.scroll-fade[data-more-above] { --fade-above: var(--space-6); }
.scroll-fade[data-more-below] { --fade-below: var(--space-6); }

/* ------------------------------------------------------------------
   Icon swap: one glyph replacing another in place (search becoming return), as SF Symbols' replace
   effect does: the leaving glyph shrinks and fades out while the arriving one grows in.
   Two glyphs in one .icon-swap; .is-alt shows the second. Opacity and scale only, so the compositor
   runs it: a swap often lands on the same frame as other main-thread work (a keystroke's results).
   ------------------------------------------------------------------ */
.icon-swap { display: inline-grid; place-items: center; }
.icon-swap > * { grid-area: 1 / 1; transition: opacity var(--fade-standard), scale var(--spring-quick); }
.icon-swap > :last-child, .icon-swap.is-alt > :first-child { opacity: 0; scale: .6; }
.icon-swap.is-alt > :last-child { opacity: 1; scale: 1; }
[data-motion="reduce"] .icon-swap > * { transition: opacity var(--fade-standard); }
@media (prefers-reduced-motion: reduce) { :root:not([data-motion="full"]) .icon-swap > * { transition: opacity var(--fade-standard); } }

/* ------------------------------------------------------------------
   Tooltip: names a control and its shortcut after a hover delay (about a second); flat, raised, a
   hairline and shadow-2, never glass. A second line may say what else the control does. It arrives
   with .arrive-fade and never takes the pointer.
   ------------------------------------------------------------------ */
.tooltip { position: fixed; z-index: var(--tooltip-z, 70); display: grid; gap: var(--space-1); padding: var(--space-2) var(--space-3);
  border: var(--space-hairline) solid var(--hairline); border-radius: var(--radius-small); background: var(--raised); box-shadow: var(--shadow-2);
  color: var(--ink); font-size: var(--type-subhead-rem); line-height: var(--type-subhead-line); white-space: nowrap; pointer-events: none; }
.tooltip-title { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); }
.tooltip kbd { font: inherit; color: var(--muted); }
.tooltip-detail { color: var(--muted); font-size: var(--type-footnote-rem); line-height: var(--type-footnote-line); }
/* A following tooltip rides with the pointer beside its control, as on Claude's sidebar edge. Its place is a
   `translate` the page retargets on each pointer move and the compositor eases over tooltip-follow, so it
   trails the pointer without any per-frame script. Both fades are transitions (@starting-style gives the
   first one its start), so leaving mid-fade-in turns smoothly; leaving, it keeps riding while it fades. */
.tooltip--follow { top: 0; opacity: 1; transition: translate var(--tooltip-follow) var(--ease-quick), opacity var(--fade-fast); }
@starting-style { .tooltip--follow { opacity: 0; } }
.tooltip--follow.is-leaving { opacity: 0; transition: translate var(--tooltip-follow) var(--ease-quick), opacity var(--fade-standard); }
[data-motion="reduce"] .tooltip--follow, [data-motion="reduce"] .tooltip--follow.is-leaving { transition: opacity var(--fade-standard); }
@media (prefers-reduced-motion: reduce) { :root:not([data-motion="full"]) :is(.tooltip--follow, .tooltip--follow.is-leaving) { transition: opacity var(--fade-standard); } }
/* A morphing tooltip (tooltip-group.tsx) is one tooltip for a group of controls, set centred under the one
   the pointer is on. Moving to a neighbour keeps it on screen: it glides there on spring-quick, its box
   eases to the new words' size, and the old words blur out while the new ones blur in, in the same place.
   Only this small box re-lays out while it resizes (contain: layout); its glide and fades are compositor
   work, and the words' blur is a few glyphs wide. */
.tooltip--morph { top: 0; opacity: 1; contain: layout; transition: translate var(--spring-quick), opacity var(--fade-fast); }
@starting-style { .tooltip--morph { opacity: 0; } }
.tooltip--morph.is-leaving { opacity: 0; transition: translate var(--spring-quick), opacity var(--fade-standard); }
/* The box (sized by the script to the arriving words) clips; each set of words is centred in it on its own,
   so while the box grows or shrinks the words stay centred and its edges reveal or trim them evenly. */
.tooltip-morph { position: relative; display: block; overflow: clip; transition: width var(--spring-quick), height var(--spring-quick); }
.tooltip-morph-words { position: absolute; top: 50%; left: 50%; translate: -50% -50%; width: max-content; transition: opacity var(--fade-fast), filter var(--fade-fast); }
@starting-style { .tooltip-morph-words { opacity: 0; filter: blur(calc(var(--reveal-blur) / 4)); } }
.tooltip-morph-words.is-leaving { opacity: 0; filter: blur(calc(var(--reveal-blur) / 4)); }
[data-motion="reduce"] :is(.tooltip--morph, .tooltip-morph, .tooltip-morph-words) { transition: opacity var(--fade-standard); filter: none; }
@media (prefers-reduced-motion: reduce) { :root:not([data-motion="full"]) :is(.tooltip--morph, .tooltip-morph, .tooltip-morph-words) { transition: opacity var(--fade-standard); filter: none; } }

/* ------------------------------------------------------------------
   Arrival: how a layer comes onto the screen. Five motions, defined once; a surface names the
   one it needs and adds nothing. Under Reduce Motion every one of them is the fade.
     .arrive-fade     a scrim, a tooltip, anything that only appears
     .arrive-rise     a centred dialog or a panel: up by space-2 from the materialize scale
     .arrive-sheet    a half sheet, from below the screen's edge
     .arrive-menu     a menu or a popover: grows from the materialize scale at its origin
     .arrive-reveal   content already in place coming into focus: clear and blurred to solid and
                      sharp (a column opening, a result landing). .arrive-reveal-each reveals the
                      element's children in turn instead, reveal-stagger apart, the fourth onward together.
   An element centred with `translate` sets --arrive-x / --arrive-y to that translation.
   An arrival runs when its class is applied, so a surface that stays mounted replays one by
   removing the class while hidden and adding it again when shown.
   ------------------------------------------------------------------ */
.arrive-fade { animation: rails-fade-in var(--fade-standard) both; }
.arrive-fade--fast { animation-duration: var(--fade-fast-duration); }
.arrive-rise { animation: rails-rise-in var(--spring-standard) both; }
.arrive-sheet { animation: rails-sheet-up var(--spring-standard) both; }
.arrive-menu { animation: rails-menu-in var(--spring-standard) both; }
@keyframes rails-fade-in { from { opacity: 0; } }
@keyframes rails-rise-in { from { opacity: 0; translate: var(--arrive-x, 0) calc(var(--arrive-y, 0px) + var(--space-2)); scale: var(--glass-materialize-scale); } }
@keyframes rails-sheet-up { from { translate: 0 100%; } }
@keyframes rails-menu-in { from { opacity: 0; scale: var(--glass-materialize-scale); } }
/* The reveal runs entirely on the compositor, so it stays smooth however busy the page is. Animating
   `filter: blur()` would not: a filter that moves pixels is never composited, so every frame would be
   restyled, repainted and re-rastered on the main thread. Instead the content fades in (opacity) under
   a veil that blurs whatever is behind it (backdrop-filter, drawn by the compositor) and fades out
   (opacity). The veil is the element's ::after, so an element whose own ::after is taken reveals with
   the fade alone. */
.arrive-reveal, .arrive-reveal-each > * { animation: rails-fade-in var(--spring-quick) backwards; }
.arrive-reveal-each > :nth-child(2) { animation-delay: var(--reveal-stagger); }
.arrive-reveal-each > :nth-child(3) { animation-delay: calc(2 * var(--reveal-stagger)); }
.arrive-reveal-each > :nth-child(n + 4) { animation-delay: calc(3 * var(--reveal-stagger)); }
:where(.arrive-reveal, .arrive-reveal-each) { position: relative; }
.arrive-reveal::after, .arrive-reveal-each::after {
  content: ""; position: absolute; inset: 0; z-index: 5; border-radius: inherit; pointer-events: none; opacity: 0;
  -webkit-backdrop-filter: blur(var(--reveal-blur)); backdrop-filter: blur(var(--reveal-blur));
  animation: rails-reveal-veil var(--spring-quick) backwards;
}
.arrive-reveal-each::after { animation-duration: calc(var(--spring-quick-duration) + 3 * var(--reveal-stagger)); }
@keyframes rails-reveal-veil { from { opacity: 1; } }

/* ------------------------------------------------------------------
   Skeleton: where content will be. Still: the system has no pulse. It fades in, so a fast load
   never flashes it.
   ------------------------------------------------------------------ */
.skeleton { display: block; overflow: hidden; border-radius: var(--radius-small); background: var(--fill-2); animation: rails-fade-in var(--fade-slow) both; }

.menu { border-radius: var(--radius-panel); padding: var(--menu-padding); --r: var(--radius-panel); display: grid; }
.menu-item {
  display: flex; align-items: center; gap: var(--space-3); height: var(--space-touch); padding: 0 var(--space-3);
  border-radius: calc(var(--radius-panel) - var(--menu-padding)); width: 100%; border: 0; background: none; text-align: left; cursor: pointer;
  font-weight: var(--type-headline-weight); color: inherit; text-decoration: none; transition: background var(--fade-fast);
}
.menu-item:hover { background: var(--fill-1); text-decoration: none; }
.menu-item:active { background: var(--fill-2); }
.menu-item--destructive { color: var(--danger); }
.menu-item .foot { margin-left: auto; font-size: var(--type-footnote-rem); line-height: var(--type-footnote-line); font-weight: var(--type-footnote-weight); }

/* ------------------------------------------------------------------
   Message bubbles: flat, never glass. Incoming has no fill; outgoing is fill-2 or the agent's
   tint at 10% (22% in dark) when it has one.
   ------------------------------------------------------------------ */
.msg { display: flex; align-items: flex-end; gap: var(--space-2); }
.msg .bubble { padding: var(--message-padding-y) var(--space-3); border-radius: var(--radius-message); max-width: calc(var(--message-max-width) * 100%); }
.msg--in .bubble { background: none; padding-left: 0; }
.msg--out { justify-content: flex-end; }
.msg--out .bubble { background: var(--fill-2); }
.msg--out.msg--tinted .bubble { background: color-mix(in oklch, var(--agent, var(--accent)) calc(var(--bubble-tint-alpha) * 100%), transparent); }

/* Reduce Motion: no press scale on any control; a toast fades in. */
[data-motion="reduce"] .btn:active { scale: 1; }
[data-motion="reduce"] .toast--placed, [data-motion="reduce"] .arrive-rise, [data-motion="reduce"] .arrive-sheet, [data-motion="reduce"] .arrive-menu, [data-motion="reduce"] .arrive-reveal, [data-motion="reduce"] .arrive-reveal-each > * { animation: rails-fade-in var(--fade-standard) both; }
[data-motion="reduce"] :is(.arrive-reveal, .arrive-reveal-each)::after { display: none; }
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .btn:active { scale: 1; }
  :root:not([data-motion="full"]) .toast--placed, :root:not([data-motion="full"]) .arrive-rise, :root:not([data-motion="full"]) .arrive-sheet, :root:not([data-motion="full"]) .arrive-menu, :root:not([data-motion="full"]) .arrive-reveal, :root:not([data-motion="full"]) .arrive-reveal-each > * { animation: rails-fade-in var(--fade-standard) both; }
  :root:not([data-motion="full"]) :is(.arrive-reveal, .arrive-reveal-each)::after { display: none; }
}
