Add a design system for Claude Design, generated from garage_ui

Tokens come out of garage_ui/tool/export_design_tokens_test.dart, the
components are CSS on top of them, and the cards cover foundations,
components and whole hub and Arcs & Angles screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
ImBenji
2026-09-23 23:19:02 +01:00
co-authored by Claude Opus 5.5
parent 572c09039f
commit b5e5ed5b75
25 changed files with 3381 additions and 0 deletions
+538
View File
@@ -0,0 +1,538 @@
/* garage_ui components, in plain css. Built on tokens.css - every value here
is a token or a formula the flutter package uses too. If something needs a
new number, it goes in garage_ui first and comes out through the export.
The odd literal is only here because the dart source has the same literal,
and its commented where it happens.
Set data-scheme / data-density on <html> (or any container) and everything
below follows.
States: every :hover / :active has an .is-hovered / .is-active twin, so a
card can show a state sat still without anyone pointing at it. */
@import url("tokens.css");
*, *::before, *::after { box-sizing: border-box; }
body, .gu {
margin: 0;
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
font-size: var(--font-size);
line-height: var(--line-height);
font-weight: var(--weight-body);
-webkit-font-smoothing: antialiased;
}
/* a nested scheme / density container has to re-resolve the things body set,
or it just inherits the outer values and only the components change */
[data-scheme] { background: var(--background); color: var(--foreground); }
[data-density] { font-size: var(--font-size); line-height: var(--line-height); }
/* ---- type ------------------------------------------------------------ */
.text-xxs { font-size: var(--text-xxs); }
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-lg { font-size: var(--text-lg); line-height: 1.3; }
.medium { font-weight: var(--weight-medium); }
.semibold { font-weight: var(--weight-semibold); }
.bold { font-weight: 700; }
.muted { color: var(--muted-foreground); }
.mono { font-family: var(--font-mono); font-weight: 400; }
/* PageHeading - title a step up, muted line under it, gapXs in from the side
(the margin a properties section carries). trailing sits gapMd off the
heading, centred on the whole block not the title line */
.page-heading { padding: 0 var(--gap-xs); display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--gap-md); align-items: center; }
.page-heading > :not(.trailing) { grid-column: 1; }
.page-heading .title { font-size: var(--text-lg); font-weight: var(--weight-control); line-height: 1.3; }
.page-heading .subtitle { margin-top: var(--gap-xs); color: var(--muted-foreground); }
/* the hub puts a gapXs between two asides when theres more then one */
.page-heading > .trailing { grid-column: 2; grid-row: 1 / span 2; display: flex; align-items: center; gap: var(--gap-xs); }
/* subHeading - the same shape one size down, more room over it than under */
.sub-heading { padding: var(--gap-xxl) var(--gap-xs) var(--gap-lg); display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--gap-md); align-items: center; }
.sub-heading > :not(.action) { grid-column: 1; }
.sub-heading .title { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.sub-heading .subtitle { margin-top: var(--gap-xxs); font-size: var(--text-xxs); color: var(--muted-foreground); }
.sub-heading > .action { grid-column: 2; grid-row: 1 / span 2; display: flex; align-items: center; gap: var(--gap-xs); }
.caption { font-size: var(--text-xxs); font-weight: var(--weight-semibold); color: var(--muted-foreground); }
/* ---- icons ----------------------------------------------------------- */
/* lucide, as inline svg. size comes from where it sits, never the call site */
.icon { width: var(--icon-sm); height: var(--icon-sm); flex: none; stroke: currentColor; fill: none;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.icon-md { width: var(--icon-md); height: var(--icon-md); }
.icon-lg { width: var(--icon-lg); height: var(--icon-lg); }
/* ---- buttons --------------------------------------------------------- */
/* every variant shares the same control padding - only the density moves it.
link and text included, they just dont paint a box */
.btn {
display: inline-flex; align-items: center; justify-content: center;
gap: var(--control-gap);
height: var(--control-height);
padding: 0 var(--control-padding-x);
border: var(--control-border-width) solid transparent;
border-radius: var(--radius-md);
background: transparent;
color: var(--foreground);
font: inherit; font-size: var(--font-size); font-weight: var(--weight-control);
white-space: nowrap; cursor: pointer; user-select: none;
outline: none;
}
.btn:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent); }
.btn:disabled, .btn.disabled { cursor: default; color: var(--muted-foreground); }
.btn.icon-only { width: var(--control-height); padding: 0; }
.btn.stretch { display: flex; width: 100%; }
.btn.align-left { justify-content: flex-start; }
.btn.primary { background: var(--primary); border-color: var(--control-border); color: var(--primary-foreground); font-weight: var(--weight-semibold); }
.btn.primary:hover:not(:disabled), .btn.primary.is-hovered { background: var(--primary-hovered); }
.btn.secondary { background: var(--secondary); border-color: var(--control-border); color: var(--secondary-foreground); }
.btn.secondary:hover:not(:disabled), .btn.secondary.is-hovered { background: var(--secondary-hovered); }
/* outline shares the text field's fill + stroke on purpose - one family */
.btn.outline { background: var(--control-fill); border-color: var(--control-border); }
.btn.outline .icon { color: var(--muted-foreground); }
.btn.outline:hover:not(:disabled), .btn.outline.is-hovered { background: var(--control-fill-hovered); }
/* ghost tints what its standing on instead of painting a colour */
.btn.ghost:hover:not(:disabled), .btn.ghost.is-hovered { background: color-mix(in srgb, var(--foreground) 7%, transparent); }
.btn.ghost:active:not(:disabled), .btn.ghost.is-active { background: color-mix(in srgb, var(--foreground) 12%, transparent); }
.btn.ghost-destructive { color: var(--destructive); }
.btn.ghost-destructive:hover:not(:disabled), .btn.ghost-destructive.is-hovered { background: color-mix(in srgb, var(--destructive) 12%, transparent); }
.btn.ghost-destructive:active:not(:disabled), .btn.ghost-destructive.is-active { background: color-mix(in srgb, var(--destructive) 20%, transparent); }
/* white is a literal in button.dart - "straight up white regardless of light or dark mode" */
.btn.destructive { background: color-mix(in srgb, var(--destructive) 50%, transparent); color: #fff; border-color: transparent; }
.btn.destructive:hover:not(:disabled), .btn.destructive.is-hovered { background: color-mix(in srgb, var(--destructive) 80%, transparent); }
.btn.link:hover:not(:disabled), .btn.link.is-hovered { text-decoration: underline; }
.btn.text { color: var(--muted-foreground); }
.btn.text:hover:not(:disabled), .btn.text.is-hovered { color: var(--primary); }
/* disabled, per variant, straight off button.dart. primary goes to a muted
foreground slab so its label still reads; secondary keeps its fill and
only the label dims; destructive swaps to the primary foreground */
.btn.primary:disabled, .btn.primary.disabled { background: var(--muted-foreground); color: var(--primary-foreground); }
.btn.secondary:disabled, .btn.secondary.disabled { background: var(--secondary); color: var(--muted-foreground); }
.btn.ghost-destructive:disabled, .btn.ghost-destructive.disabled { background: transparent; color: var(--muted-foreground); }
.btn.destructive:disabled, .btn.destructive.disabled { background: var(--primary-foreground); color: var(--muted-foreground); }
/* text has no disabled look in the source, it just stays muted */
.btn.text:disabled, .btn.text.disabled { color: var(--muted-foreground); }
/* button group - joined, only the outer corners round */
.btn-group { display: inline-flex; }
.btn-group > * { border-radius: 0; }
.btn-group > *:first-child { border-top-left-radius: var(--radius-md); border-bottom-left-radius: var(--radius-md); }
.btn-group > *:last-child { border-top-right-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); }
.btn-group > * + * { margin-left: calc(var(--control-border-width) * -1); }
.btn-group.fill { display: flex; }
.btn-group.fill > * { flex: 1; }
/* span: the group takes the width its given and ONE member (.grow) takes the
slack - a field with its IconButton welded on, the button keeping its square */
.btn-group.span { display: flex; width: 100%; }
.btn-group > .grow { flex: 1; min-width: 0; }
.btn-group.vertical { flex-direction: column; }
.btn-group.vertical > * { border-radius: 0; }
.btn-group.vertical > *:first-child { border-radius: var(--radius-md) var(--radius-md) 0 0; }
.btn-group.vertical > *:last-child { border-radius: 0 0 var(--radius-md) var(--radius-md); }
.btn-group.vertical > *:only-child { border-radius: var(--radius-md); }
.btn-group.vertical > * + * { margin-left: 0; margin-top: calc(var(--control-border-width) * -1); }
/* a group inside a group - a corner only rounds where BOTH groups say so
(ButtonGroupCorners.merge, its a product of the two) */
.btn-group:not(.vertical) > .btn-group:not(:first-child) > * { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.btn-group:not(.vertical) > .btn-group:not(:last-child) > * { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.btn-group.vertical > .btn-group:not(:first-child) > * { border-top-left-radius: 0; border-top-right-radius: 0; }
.btn-group.vertical > .btn-group:not(:last-child) > * { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
/* ---- text fields ----------------------------------------------------- */
.field {
display: inline-flex; align-items: center; gap: var(--control-gap);
height: var(--control-height);
padding: 0 calc(var(--control-padding-x) - var(--control-border-width));
background: var(--control-fill);
border: var(--control-border-width) solid var(--control-border);
border-radius: var(--radius-md);
color: var(--foreground);
font-weight: var(--weight-control);
min-width: 0;
}
/* only an outline field that can be typed in reacts - secondary is a flat
secondary fill in every state, and a read only one is just looked at */
.field:not(.secondary):not(.readonly):hover, .field:not(.secondary):not(.readonly).is-hovered { background: var(--control-fill-hovered); }
.field:not(.secondary):not(.readonly):focus-within, .field:not(.secondary):not(.readonly).is-focused { background: var(--control-fill-focused); }
.field input, .field textarea {
all: unset; flex: 1; min-width: 0; width: 100%;
font-family: var(--font-sans); font-size: var(--font-size); font-weight: var(--weight-control);
color: inherit;
}
.field input::placeholder { color: var(--muted-foreground); }
.field .icon, .field .unit { color: var(--muted-foreground); }
.field.secondary { background: var(--secondary); color: var(--secondary-foreground); }
.field.secondary .icon { color: var(--secondary-foreground); }
/* one rule for "you cant change this" - the value AND the icons beside it */
.field.readonly, .field.readonly .icon { color: var(--muted-foreground); }
.field.center input { text-align: center; }
/* ValueField(monospace:) - for ids and skus, read a character at a time */
.field.mono input { font-family: var(--font-mono); }
/* a rejected value: the outline goes full destructive, never the fill */
.field.error { border-color: var(--destructive); }
.field.stretch { display: flex; width: 100%; }
/* ---- select ---------------------------------------------------------- */
/* the value sits in the middle of the TRIGGER - a chevron's worth of nothing
on the left balances the chevron on the right */
.select { display: inline-flex; align-items: center; justify-content: space-between; cursor: pointer; gap: var(--control-gap);
height: var(--control-height); padding: 0 calc(var(--control-padding-x) - var(--control-border-width));
background: var(--control-fill); border: var(--control-border-width) solid var(--control-border);
border-radius: var(--radius-md); color: var(--foreground); font-weight: var(--weight-control); white-space: nowrap; }
.select::before { content: ""; flex: none; width: var(--icon-sm); }
.select > span { flex: 1; min-width: 0; text-align: center; }
.select .placeholder { color: var(--muted-foreground); }
.select .chevron { color: color-mix(in srgb, currentColor 50%, transparent); }
.select:hover, .select.is-hovered { background: var(--control-fill-hovered); }
.select.secondary { background: var(--secondary); color: var(--secondary-foreground); }
.select.secondary:hover, .select.secondary.is-hovered { background: var(--secondary-hovered); }
.select.ghost { background: transparent; border-color: transparent; }
.select.ghost:hover, .select.ghost.is-hovered { background: var(--popover-item-hovered); }
.select.disabled { color: var(--muted-foreground); cursor: default; }
.select.disabled:hover { background: var(--control-fill); }
.select.secondary.disabled:hover { background: var(--secondary); }
.select.ghost.disabled:hover { background: transparent; }
.select.error { border-color: var(--destructive); }
/* the select popup - its own surface, not a menu. inset is half the container
gap, the corner is concentric with the rows (less 2, a literal in
select.dart - the full sum read too soft) */
.select-popup {
display: inline-flex; flex-direction: column; gap: var(--gap-xxs);
padding: calc(var(--container-gap) * 0.5);
background: var(--popover);
border: var(--control-border-width) solid var(--popover-border);
border-radius: calc(var(--radius-md) + var(--container-gap) * 0.5 + var(--control-border-width) - 2px);
color: var(--foreground);
}
.select-popup .option {
display: flex; align-items: center; justify-content: space-between; gap: var(--control-gap);
height: var(--popup-row-height);
padding: 0 calc(var(--control-padding-x) - var(--control-border-width));
border: var(--control-border-width) solid transparent;
border-radius: var(--radius-md);
font-weight: var(--weight-control); white-space: nowrap; cursor: default;
}
.select-popup .option:hover, .select-popup .option.is-hovered { background: var(--popover-item-hovered); }
.select-popup .option.disabled { color: var(--muted-foreground); }
/* the selected row IS a secondary button, stroke and all, with the check */
.select-popup .option.selected { background: var(--secondary); border-color: var(--control-border); color: var(--secondary-foreground); }
.select-popup .option.selected:hover, .select-popup .option.selected.is-hovered { background: var(--secondary-hovered); }
/* once anything is selected every row reserves the check column, mirrored on
the left, so the labels stay centred on the row */
.select-popup:has(.option.selected) .option::before,
.select-popup:has(.option.selected) .option:not(.selected)::after { content: ""; flex: none; width: var(--icon-sm); }
/* ---- popovers, menus, tooltips --------------------------------------- */
/* MenuPopup - the 2px inset and 1px border are literals in menu.dart */
.popover {
display: inline-flex; flex-direction: column;
background: var(--popover);
border: 1px solid var(--popover-border);
border-radius: var(--radius-md);
padding: 2px;
font-weight: var(--weight-control);
}
/* a row: an 18px icon slot and a 4px gap after it, the icon nudged toward the
edge by (4 - padding) / 2. 18, 4 and the 4px row radius are all literals in
menu.dart */
.menu-item {
display: flex; align-items: center; gap: 4px;
height: var(--menu-row-height);
padding: 0 var(--control-padding-x);
border-radius: 4px;
color: var(--foreground); font-weight: var(--weight-control); white-space: nowrap; cursor: default;
}
.menu-item > .icon:first-child { margin: 0 calc((18px - var(--icon-sm)) / 2); position: relative; left: calc((4px - var(--control-padding-x)) / 2); }
.menu-item .icon { color: var(--muted-foreground); }
.menu-item .shortcut { margin-left: auto; padding-left: var(--container-gap); color: var(--muted-foreground); font-size: var(--font-size); }
.menu-item:hover, .menu-item.is-hovered { background: var(--row-hovered); }
.menu-item.disabled { color: color-mix(in srgb, var(--foreground) 50%, transparent); }
.menu-item.disabled .icon { color: color-mix(in srgb, var(--muted-foreground) 50%, transparent); }
.menu-item.disabled:hover { background: transparent; }
/* MenuLabel - same padding as a row, so the same height, just semibold */
.menu-label { display: flex; align-items: center; height: var(--menu-row-height); padding: 0 var(--control-padding-x); font-weight: var(--weight-semibold); white-space: nowrap; }
/* 4px either side is a literal in menu.dart */
.menu-divider { height: 1px; background: var(--divider); margin: 4px 0; }
/* TooltipContainer - three quarters of the container padding across, three
quarters of the container gap down */
.tooltip {
display: inline-block;
background: var(--tooltip-background);
border: 1px solid var(--tooltip-border);
border-radius: var(--radius-sm);
padding: calc(var(--container-gap) * 0.75) calc(var(--container-padding) * 0.75);
color: var(--foreground);
font-size: var(--font-size); font-weight: var(--weight-control);
}
/* ---- selection controls ---------------------------------------------- */
/* 16px box, 1px border - literals in selection_controls.dart */
.checkbox {
display: inline-flex; align-items: center; justify-content: center; flex: none;
width: 16px; height: 16px;
background: var(--secondary);
border: 1px solid var(--control-border);
border-radius: var(--radius-sm);
}
.checkbox.checked { background: var(--primary); border-color: var(--primary); }
/* the tick is drawn, not an icon: a 9 x 6.5 check on a 1px round stroke, same
geometry as AnimatedCheckPainter. the mask box is half a pixel bigger each
side so the round caps dont get clipped */
.checkbox.checked::after {
content: ""; width: 10px; height: 7.5px;
background: var(--primary-foreground);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-0.5 -0.5 10 7.5'%3E%3Cpolyline points='0,3.25 3.15,6.5 9,0' fill='none' stroke='black' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-0.5 -0.5 10 7.5'%3E%3Cpolyline points='0,3.25 3.15,6.5 9,0' fill='none' stroke='black' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.checkbox.indeterminate::after { content: ""; width: 8px; height: 8px; background: var(--primary); border-radius: var(--radius-xs); }
.checkbox.disabled { border-color: var(--muted); }
/* (32 + 4) x (16 + 4) is literal in the source; the inset is a quarter of the
container gap and the thumb is a square filling whats left */
.switch {
display: inline-flex; align-items: stretch; flex: none;
width: 36px; height: 20px; padding: calc(var(--container-gap) * 0.25);
background: var(--switch-track-inactive);
border-radius: var(--radius-xl);
}
.switch::after { content: ""; height: 100%; aspect-ratio: 1; border-radius: var(--radius-lg); background: var(--foreground); }
.switch.on { background: var(--primary); justify-content: flex-end; }
.switch.on::after { background: var(--background); }
.switch.disabled { background: var(--muted); }
.switch.disabled::after { background: var(--muted-foreground); }
/* ---- surfaces & layout ----------------------------------------------- */
/* the whole look: chrome ground, panels standing on it, panel-gap gutters */
.shell { background: var(--chrome); padding: var(--panel-gap); display: flex; gap: var(--panel-gap); }
.panel {
background: var(--background);
border: var(--panel-border-width) solid var(--panel-border);
border-radius: var(--panel-radius);
overflow: hidden;
}
.panel.active { border-color: var(--panel-border-highlighted); }
.panel.sunken { background: var(--surface-sunken); }
.chrome-bar {
display: flex; align-items: center; gap: var(--gap-sm);
height: var(--chrome-bar-height);
background: var(--chrome);
padding: 0 var(--gap-lg);
}
.chrome-bar .vr { width: 1px; height: 12px; background: var(--divider); }
.panel-header { display: flex; align-items: center; gap: 2px; padding: 10px 10px 8px; }
.panel-header .icon { width: 12px; height: 12px; margin: 0 3px; color: var(--foreground); }
.panel-header .title { font-weight: var(--weight-medium); flex: 1; }
/* a nav rail's category, as the hub draws it - gapMd round the run, a
semibold caption over it, gapXxs between the buttons only */
.nav-group { padding: var(--gap-md); display: flex; flex-direction: column; gap: var(--gap-xxs); }
.nav-group > .caption { padding: 0 var(--gap-sm) calc(var(--gap-xs) - var(--gap-xxs)); }
/* explorer tree rows (A&A). an 18px slot for the chevron and one for the
icon, 2px between, 12px glyphs - all literals in explorer_panel.dart. the
hover is inset 2px with a 3px corner, same file */
.explorer-row {
--depth: 0;
position: relative; isolation: isolate;
display: flex; align-items: center; gap: 2px;
height: var(--explorer-row-height);
padding: 0 var(--explorer-row-end-padding) 0 calc(var(--explorer-row-base-padding) + var(--depth) * 18px);
color: var(--row-text); white-space: nowrap;
}
.explorer-row.depth-1 { --depth: 1; }
.explorer-row.depth-2 { --depth: 2; }
.explorer-row.depth-3 { --depth: 3; }
.explorer-row > .slot { flex: none; width: 18px; display: flex; align-items: center; justify-content: center; }
.explorer-row .icon { width: 12px; height: 12px; }
.explorer-row .chevron { color: var(--muted-foreground); }
.explorer-row > .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.explorer-row:hover::before, .explorer-row.is-hovered::before {
content: ""; position: absolute; inset: 2px; z-index: -1;
background: var(--row-hovered); border-radius: 3px;
}
.card {
background: var(--card);
border: 1px solid var(--properties-section-border);
border-radius: var(--radius-xl);
padding: var(--container-padding);
}
.outlined {
background: var(--background);
border: 1px solid var(--control-border);
border-radius: var(--radius-xl);
}
.divider { height: 1px; background: var(--divider); border: 0; margin: 0; }
/* ---- properties section --------------------------------------------- */
.properties-section {
margin: 0 var(--gap-xs) var(--properties-section-gap);
background: var(--card);
border: 1px solid var(--properties-section-border);
border-radius: var(--radius-sm);
overflow: hidden;
}
.properties-section > .header {
display: flex; align-items: center; gap: var(--gap-sm);
padding: var(--control-padding-y) var(--control-padding-x);
color: var(--row-text); font-weight: var(--weight-semibold);
}
.properties-section > .header .chevron { color: var(--muted-foreground); }
/* the header's own subtitle - captionStyle, so the ambient weight not the title's */
.properties-section > .header .subtitle { display: block; font-size: var(--text-xxs); color: var(--muted-foreground); font-weight: var(--weight-body); }
.properties-section > .header .trailing { margin-left: auto; font-weight: var(--weight-control); }
/* the last row gets more room under it than the step between rows */
.properties-section > .rows { padding: 0 var(--control-padding-x) var(--gap-md); display: flex; flex-direction: column; gap: var(--gap-xs); }
.properties-section > .actions {
display: flex; justify-content: flex-end; gap: var(--gap-sm);
border-top: 1px solid var(--properties-section-border);
background: color-mix(in srgb, var(--card) 55%, var(--muted));
padding: var(--gap-sm) var(--control-padding-x);
}
/* PropertyRow - the label up to the split, the control from it. the gap comes
out of the label's padding, so the control's edge lands right on the split.
it wraps, so an .error or .description can take a line of its own under it */
.property-row { display: flex; flex-wrap: wrap; align-items: center; }
.property-row > .label { flex: 0 0 calc(var(--property-split) * 100%); padding-right: var(--property-label-gap); min-width: 0; color: var(--row-text); text-align: right; }
.property-row > .label .subtitle { display: block; font-size: var(--text-xxs); color: var(--muted-foreground); }
.property-row > .label .tag { font-size: var(--text-xxs); color: var(--muted-foreground); margin-left: var(--gap-xs); }
.property-row > .value { flex: 1 1 0; min-width: 0; min-height: var(--property-row-height); display: flex; align-items: center; gap: var(--control-gap); }
.property-row > .value > .field, .property-row > .value > .select, .property-row > .value > .btn-group.span { flex: 1; }
/* why the value was refused - under the row, centred, destructive */
.property-row > .error { flex: 0 0 100%; margin-top: var(--gap-xs); text-align: center; font-size: var(--text-xxs); color: var(--destructive); }
/* copy about the setting, in a box that matches the outline controls */
.property-row > .description {
flex: 0 0 100%; margin-top: var(--gap-xs);
padding: var(--gap-xs) var(--control-padding-x);
background: var(--control-fill);
border: var(--control-border-width) solid var(--control-border);
border-radius: var(--radius-md);
text-align: center; font-size: var(--text-xxs); color: var(--muted-foreground);
}
/* ---- settings list --------------------------------------------------- */
/* ruled BETWEEN rows, and the rule overshoots the text by the row inset so it
reads as a divider rather than the edge of a box */
.settings-list { display: flex; flex-direction: column; }
.settings-list > .settings-row + .settings-row { border-top: 1px solid var(--divider); }
.settings-row { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--gap-md); padding: var(--gap-md) var(--gap-sm); }
.settings-row > .label { flex: 1 1 0; min-width: 0; font-size: var(--text-xs); font-weight: var(--weight-control); color: var(--foreground); }
.settings-row > .label .subtitle { display: block; margin-top: var(--gap-xxs); font-size: var(--text-xxs); font-weight: var(--weight-body); color: var(--muted-foreground); }
.settings-row > .label .tag { font-size: var(--text-xxs); font-weight: var(--weight-control); color: var(--muted-foreground); margin-left: var(--gap-xs); }
/* a loose Flexible: at most what the label leaves, field pinned right. a
select or a button keeps its own width, a text field fills the slot.
the minimum lives here rather than on the label so the label can stay a
plain block - its the height of a row WITH a subtitle, so a list mixing the
two doesnt come out ragged */
.settings-row > .field-slot {
flex: 1 1 0; min-width: 0;
min-height: calc(var(--label-column-height) + var(--gap-xxs));
display: flex; align-items: center; justify-content: flex-end;
}
.settings-row > .field-slot > .field { flex: 1; }
.settings-row > .error { flex: 0 0 100%; margin-top: var(--gap-sm); font-size: var(--text-xxs); color: var(--destructive); }
.settings-row > .description { flex: 0 0 100%; margin-top: var(--gap-sm); font-size: var(--text-xxs); color: var(--muted-foreground); }
.settings-row > .error + .description { margin-top: var(--gap-xxs); }
/* ---- dialogs, toasts ------------------------------------------------- */
/* AlertDialog's barrier - black at 0.8, no blur */
.modal-backdrop { background: rgb(0 0 0 / 0.8); }
/* pane overlays keep the lighter frosted scrim */
.scrim { background: color-mix(in srgb, var(--background) 35%, transparent); backdrop-filter: blur(3px); }
.dialog {
width: 100%; max-width: var(--dialog-max-width);
background: var(--card);
border: 1px solid var(--properties-section-border);
border-radius: var(--radius-xxl);
overflow: hidden;
}
.dialog > .body { padding: calc(var(--container-padding) * 1.2) calc(var(--container-padding) * 1.4) calc(var(--container-padding) * 0.6); display: flex; flex-direction: column; gap: var(--container-gap); }
.dialog > .body .title { font-size: var(--text-xs); font-weight: var(--weight-semibold); }
.dialog > .body .content { font-size: var(--text-xs); color: var(--muted-foreground); }
.dialog > .actions {
display: flex; justify-content: flex-end; gap: var(--container-gap);
border-top: 1px solid var(--properties-section-border);
background: color-mix(in srgb, var(--card) 55%, var(--muted));
padding: var(--gap-sm) var(--control-padding-x);
}
/* the desktop toast. 300 wide, the 32px badge, the 18px glyph, the white on
it and the 3px bar are all literals in toast.dart */
.toast {
--toast-accent: var(--primary);
display: flex; flex-direction: column;
width: 300px;
background: var(--popover);
border: 1px solid var(--popover-border);
border-radius: var(--radius-md);
overflow: hidden;
}
.toast.success { --toast-accent: var(--chart-2); }
.toast.error { --toast-accent: var(--destructive); }
.toast > .body { display: flex; align-items: flex-start; gap: var(--container-gap); padding: var(--container-gap); }
.toast .badge {
flex: none; width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
border: var(--control-border-width) solid var(--control-border);
border-radius: var(--radius-md);
color: #fff;
}
.toast .badge .icon { width: 18px; height: 18px; }
.toast .text { flex: 1; min-width: 0; }
/* title and subtitle are the same text, told apart by colour only */
.toast .title { font-weight: var(--weight-control); color: var(--foreground); }
.toast .subtitle { margin-top: calc(var(--container-gap) * 0.375); font-weight: var(--weight-control); color: var(--muted-foreground); }
/* the countdown - the accent drains off the right, over the border colour at 0.4 */
.toast > .drain { display: flex; height: 3px; background: color-mix(in srgb, var(--popover-border) 40%, transparent); }
.toast > .drain > span { background: var(--toast-accent); }
/* CircularProgressIndicator - no track, a 1.35pi arc (~243deg) on a 4px
round stroke, 18px, primary, once round every 900ms. all literal in
misc.dart. the two dots are the round caps */
.spinner {
position: relative; flex: none;
width: 18px; height: 18px; border-radius: 50%;
background: conic-gradient(var(--primary) 0 243deg, transparent 0);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
animation: gu-spin 0.9s linear infinite;
}
.spinner::before, .spinner::after {
content: ""; position: absolute; left: 7px; top: 0;
width: 4px; height: 4px; border-radius: 50%;
background: var(--primary);
transform-origin: 2px 9px;
}
.spinner::after { transform: rotate(243deg); }
@keyframes gu-spin { to { transform: rotate(360deg); } }