/* Tinkerex component vocabulary. ONE DEFINITION PER COMPONENT SELECTOR, HERE AND NOWHERE ELSE:
   the pre-2026-09 sheets defined `.btn` twice (application.css blue, projects.css grey) and the
   later file won, so `.btn` rendered grey everywhere and `.btn.primary` blue. A page sheet under
   pages/ may style its own classes and may qualify a component inside a page container
   (`.files-grid .thumb`), but never redeclares a bare component selector from this file.
   test/css_hygiene_test.rb fails if one does. Every colour is a token from application.css. */
@layer components {
  /* --- Buttons --- */
  .btn, input[type="submit"], .upload-btn span, details.composer > summary {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
    min-height: var(--control-height); padding: 0 14px; border-radius: var(--radius-control);
    border: 1px solid var(--line-strong); background: var(--surface); color: var(--fg);
    font: 500 14px/1 var(--font-ui); text-decoration: none; cursor: pointer; white-space: nowrap;
    transition: background-color .12s, border-color .12s;
  }
  .btn:hover, input[type="submit"]:hover, .upload-btn span:hover, details.composer > summary:hover { background: var(--raised); text-decoration: none; }
  .btn svg { width: 16px; height: 16px; flex: 0 0 auto; }
  /* THE ACCENT MEANS "THE ONE ACTION THIS PAGE EXISTS FOR", so an uploader is in this group only
     when its page ASKS -- `.upload-btn.primary`, not `.upload-btn`. It was unconditional until
     2026-09-10, which meant no page could ask for a neutral uploader and every upload control in
     the app was the loudest thing on its page BY CONSTRUCTION. Measured on a project with three
     build-log entries: seven accent controls, four of them the same "Upload & attach" repeated per
     log row, so nothing on the page read as THE thing to do. The neutral `.btn` face still comes
     from the base rule above, so an opted-out uploader is a button, not an unstyled label.
     Guard: test/system/accent_spend_test.rb, which counts RESOLVED backgrounds -- a class-list
     assertion counted zero on the page that had ten. */
  .btn.primary, .upload-btn.primary span, input[type="submit"].primary {
    background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600;
  }
  .btn.primary:hover, .upload-btn.primary span:hover, input[type="submit"].primary:hover { filter: brightness(1.06); border-color: var(--accent); }
  .btn.danger { background: transparent; border-color: var(--danger); color: var(--danger); }
  .btn.danger:hover { background: var(--danger-soft); }
  .btn.ghost { background: transparent; border-color: transparent; }
  .btn.ghost:hover { background: var(--raised); }
  .btn.small, input[type="submit"].small { min-height: var(--control-height-small); padding: 0 10px; font-size: 13px; }
  .btn.icon { width: var(--control-height); padding: 0; }
  .btn.small.icon { width: var(--control-height-small); }
  .btn:disabled, input[type="submit"]:disabled { opacity: .5; cursor: default; filter: none; }
  .linkbtn {
    background: none; border: 0; padding: 0; color: var(--fg); font: inherit; cursor: pointer;
    text-decoration: underline; text-decoration-color: var(--line-strong); text-underline-offset: 3px;
  }
  .linkbtn:hover { text-decoration-color: var(--fg); }
  .linkbtn.danger { color: var(--danger); text-decoration-color: currentColor; }
  .linkbtn.small { font-size: 12px; }
  .upload-btn { display: inline-flex; cursor: pointer; }
  .upload-status { font-size: 12px; color: var(--fg-muted); }
  .uploader { display: inline-flex; align-items: center; gap: var(--space-3); }

  /* --- Inputs --- */
  .app-shell input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="submit"]):not([type="hidden"]),
  .app-shell select, .app-shell textarea,
  .auth-card input:not([type="checkbox"]):not([type="submit"]) {
    font: 400 14px/20px var(--font-ui); color: var(--fg); background: var(--surface);
    border: 1px solid var(--line-strong); border-radius: var(--radius-control);
    padding: 5px 10px; min-height: var(--control-height);
  }
  .app-shell input:hover, .app-shell select:hover, .app-shell textarea:hover,
  .auth-card input:hover { border-color: var(--fg-muted); }
  .app-shell input:focus-visible, .app-shell select:focus-visible, .app-shell textarea:focus-visible,
  .auth-card input:focus-visible { border-color: var(--fg); outline: 2px solid var(--focus); outline-offset: 1px; }
  .app-shell input::placeholder, .app-shell textarea::placeholder { color: var(--fg-muted); }
  .app-shell textarea { min-height: 88px; line-height: 22px; }
  .app-shell select {
    padding-right: 28px; appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23878276' stroke-width='1.5'/></svg>");
    background-repeat: no-repeat; background-position: right 10px center;
  }
  .field { display: block; margin-bottom: var(--space-4); }
  .field > span, .field > label { display: block; margin-bottom: var(--space-1); font-size: 13px; font-weight: 500; color: var(--fg); }
  .field input, .field textarea, .field select { width: 100%; max-width: 480px; }
  .field > .folder-field { display: inline-flex; }
  .field-inline { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); font-size: 13px; }
  .field-inline input { width: auto; }
  .form-actions { display: flex; gap: 12px; align-items: center; }
  .form-errors {
    margin-bottom: var(--space-4); padding: 10px 12px; border-radius: var(--radius-control);
    border: 1px solid var(--danger); background: var(--danger-soft); color: var(--fg); font-size: 13px;
  }

  /* --- Chips, segmented control, toolbar, breadcrumb, pager, page head --- */
  .chip {
    display: inline-flex; align-items: center; gap: var(--space-1); min-height: var(--control-height-small);
    padding: 0 10px; border-radius: var(--radius-pill); border: 1px solid var(--line);
    background: var(--surface); color: var(--fg-muted); font: 500 13px/1 var(--font-ui); text-decoration: none;
  }
  .chip:hover { border-color: var(--line-strong); color: var(--fg); text-decoration: none; }
  .chip.active { border-color: var(--fg); background: var(--fg); color: var(--surface); }
  .chip .chip-x { opacity: .75; font-weight: 600; }
  .chip.active:hover { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
  /* Tags (stage 2, S2-5): the chip row a tagged record's page renders (TagsHelper#tag_chips) and
     the tag page's own h1. Smaller, lowercase and outlined so a run of them reads as metadata
     beside a title, not as a second row of the same buttons `.chip` renders for a toolbar filter. */
  .tag-chips { display: flex; flex-wrap: wrap; gap: 6px; }
  .tag-chip {
    display: inline-flex; align-items: center; padding: 2px 8px; border-radius: var(--radius-pill);
    border: 1px solid var(--line); background: var(--surface); color: var(--fg-muted);
    font: 500 12px/16px var(--font-ui); text-transform: lowercase; text-decoration: none;
  }
  .tag-chip:hover { border-color: var(--line-strong); color: var(--fg); text-decoration: none; }
  .view-toggle { display: inline-flex; gap: 2px; padding: 2px; border-radius: var(--radius-control); background: var(--raised); }
  /* A `.toggle` is an <a> on the Files page and a button_to's <button> on the project page (folder
     panels), so the UA's button border and background are reset HERE, in the component: a reset in
     a `pages` sheet would beat `.toggle.active` and `.toggle:hover` below regardless of specificity,
     because across cascade layers only the layer order counts (found in review). */
  .view-toggle .toggle {
    display: inline-flex; align-items: center; gap: 6px; min-height: calc(var(--control-height-small) - 2px); padding: 0 10px;
    border: 0; border-radius: 2px; background: none; color: var(--fg-muted);
    font: 500 13px/1 var(--font-ui); text-decoration: none; cursor: pointer;
  }
  .view-toggle .toggle:hover { color: var(--fg); text-decoration: none; }
  .view-toggle .toggle.active { background: var(--surface); color: var(--fg); box-shadow: var(--shadow-toggle); }
  .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin: 0 0 var(--space-4); }
  .toolbar-spacer { flex: 1; }
  /* ONE COUNT, ONE DEFINITION (UI-cohesion spec §2.7). This lived TWICE — `pages/files.css` had it
     bare and `pages/inventory.css` had it qualified, with the same three declarations — which is
     the `.projects-empty` shape again and equally invisible to css_hygiene_test.rb, since
     `.toolbar-count` was not on its component list. It is now, and both copies are gone.
     MONO IS CORRECT HERE and is not the chrome-mono decision 4 retires: a count is a QUANTITY,
     which is exactly what decision 4 says mono means. `tabular-nums` so a count changing under a
     filter does not shift the controls beside it. */
  /* A toolbar part, not a Files-page one: `pages/files.css` defined it and then Projects needed
     it too (slice 4). Both uses sit inside `.toolbar`, which owns the spacing, so the standalone
     bottom margin that rule carried is gone with the move. */
  .sort-form { display: flex; align-items: center; gap: 8px; margin: 0; }
  .toolbar-count { font: 400 12px/16px var(--font-mono); font-variant-numeric: tabular-nums; color: var(--fg-muted); white-space: nowrap; }
  .breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 14px; min-width: 0; }
  .breadcrumb .sep { color: var(--line-strong); }
  .breadcrumb a { color: var(--fg-muted); }
  .breadcrumb a:hover { color: var(--fg); text-decoration: none; }
  .breadcrumb [aria-current="page"] { color: var(--fg); font-weight: 600; }
  .pager { display: flex; align-items: center; gap: var(--space-4); margin: 0 0 var(--space-3); font-size: 13px; }
  .pager-status { color: var(--fg-muted); font-family: var(--font-mono); }
  .page-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: var(--space-2) var(--space-4); margin: 0 0 var(--space-5); }
  .page-head h1 { font-size: 20px; line-height: 26px; }
  .page-subtitle { margin: 2px 0 0; font-size: 14px; color: var(--fg-muted); flex-basis: 100%; }
  .page-actions, .row-actions, .head-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

  /* --- Panels, group headers, tables --- */
  .panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-panel); padding: var(--space-4); }
  .panel h2 { margin: 0 0 var(--space-3); font-size: 16px; }
  .panel-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-3); margin-bottom: var(--space-3); }
  .panel-head h2 { margin: 0; }
  .panel-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

  /* --- The page editor (2026-09-09, spec 2026-09-09-page-editor §5): the slot, the strip along
     a panel's top edge, the ghost state, the top bar and the YAML drawer. Components, so they hold
     in both themes and the page sheet keeps only the grid rule (design-system rules 2 and 3).
     `panel--full` / `panel--half` stay in pages/projects.css: they are the GRID's business (a
     column span inside `.project-panels`' own media query), not a component's. --- */
  .panel-slot { min-width: 0; }
  .panel-intro { margin: -6px 0 var(--space-3); font-size: 13px; color: var(--fg-muted); }
  .panel-slot-body > .panel { border-top-left-radius: 0; border-top-right-radius: 0; }
  .panel--placeholder p { margin: 0; }
  .panel-strip { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 4px 8px;
                 border: 1px dashed var(--line-strong); border-bottom: 0;
                 border-radius: var(--radius-panel) var(--radius-panel) 0 0; background: var(--raised); font-size: 12px; }
  .panel-strip-handle { cursor: grab; color: var(--fg-muted); font-size: 14px; letter-spacing: -2px; user-select: none; }
  .panel-strip-name { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
  .panel-strip-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin-left: auto; }
  .panel-strip .view-toggle .toggle { padding: 2px 8px; }
  .panel-strip-narrow { font-size: 11px; }
  @media (min-width: 900px) { .panel-strip-narrow { display: none; } }
  .panel-strip-edit { position: relative; }
  .panel-strip-edit > summary { list-style: none; cursor: pointer; }
  .panel-strip-edit > summary::-webkit-details-marker { display: none; }
  .panel-strip-popover { position: absolute; right: 0; top: calc(100% + 4px); z-index: 5; width: min(320px, 80vw); display: grid; gap: 8px;
                         padding: 10px; background: var(--surface); border: 1px solid var(--line-strong);
                         border-radius: var(--radius-panel); box-shadow: var(--shadow-float); }
  .panel-slot--hidden .panel-slot-body { opacity: .45; }
  .panel-slot--hidden .panel-strip { border-style: dotted; }
  .panel-slot--ghost { opacity: .35; }
  .layout-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px;
                padding: 10px 12px; margin: 0 0 12px; border: 1px solid var(--info); border-radius: var(--radius-panel); background: var(--info-soft); }
  .layout-bar-text { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 13px; }
  .layout-bar-noscript { color: var(--danger); }
  .layout-bar-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
  .layout-bar-actions form { display: contents; }
  .layout-editor--drawer { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 16px; align-items: start; }
  .layout-editor--drawer .layout-bar { grid-column: 1 / -1; }
  .layout-drawer { position: sticky; top: 12px; display: flex; flex-direction: column; gap: 8px; padding: 12px;
                   border: 1px solid var(--line); border-radius: var(--radius-panel); background: var(--surface); }
  .layout-drawer-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
  .layout-yaml { width: 100%; min-height: 320px; resize: vertical; font: 12px/1.45 var(--font-mono); color: var(--fg);
                 background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-control); padding: 8px; }
  .layout-note { margin: 0; font-size: 12px; }
  .layout-note--error { color: var(--danger); }
  .layout-note--ok { color: var(--ok); }
  @media (max-width: 899px) { .layout-editor--drawer { grid-template-columns: 1fr; } .layout-drawer { position: static; } }

  /* --- Tabs (2026-09-09; spec 2026-09-09-tabs-design §6). The separator slot in edit mode, and the
     bar + panes on the read pages. Components, not page rules, because the visitor's page renders
     the same bar and panes from projects/_panel_groups — the identical argument the content-block
     rules below carry.

     `.tab-heading` SHOWS ONLY WITHOUT JAVASCRIPT: tabs_controller adds `tabs-ready` on connect, and
     that class is what hides the headings, so a page whose script never ran reads as headed
     sections with nothing concealed (decision 8). Hiding the heading in the base rule and revealing
     it some other way would invert that: the no-JavaScript page would lose its own structure.

     THE BAR SCROLLS SIDEWAYS RATHER THAN WRAPPING, on every width (decision 7): one row of tabs is
     one mechanism and one set of deep links, and a wrapped bar changes height as tabs are renamed. */
  .panel-slot--tab { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; padding: 8px 12px;
                     border: 1px solid var(--info); border-radius: var(--radius-panel); background: var(--info-soft); }
  .panel-slot--tab .tab-slot-label { font-weight: 600; }
  .panel-slot--tab .tab-name { width: min(240px, 60vw); }
  .panel-slot--tab .tab-slot-count { flex: 1 1 auto; font-size: 12px; }
  .tabs { display: grid; gap: 16px; margin: 16px 0; }
  .tab-bar { display: flex; gap: var(--space-5); overflow-x: auto; white-space: nowrap; border-bottom: 1px solid var(--line); scrollbar-width: thin; }
  .tab-link { padding: 10px 0; border-bottom: 2px solid transparent; margin-bottom: -1px;
              color: var(--fg-muted); font-weight: 500; text-decoration: none; }
  .tab-link:hover { color: var(--fg); text-decoration: none; }
  .tab-link.is-active, .tab-link[aria-selected="true"] { color: var(--fg); border-bottom-color: var(--fg); }
  .tab-pane { display: grid; gap: 12px; }
  .tab-heading { margin: 0; }
  .tabs.tabs-ready .tab-heading { display: none; }

  /* --- Content blocks (2026-09-09, spec 2026-09-09-content-blocks §2, §3): the three kinds as both
     pages draw them, the editors edit mode puts in their place, and the page-intro card above the
     grid. Components, not page rules, because the visitor's page renders the same block markup from
     its own partials — a rule in pages/projects.css would style the maker's copy only.

     `.image-pick` IS ONE CONTROL WITH TWO CALLERS: an image block's picker and the cover's picker
     (which adds `.cover-pick` to carry a different action, not a different look). Its `.active` is
     the ink outline (selection is ink, design-system.md rule 13), so "which picture is this
     block's" is answerable at a glance. --- */
  .layout-add .menu-list { min-width: 160px; }
  .layout-add-cap { max-width: 200px; margin: 4px 6px 2px; font-size: 11px; white-space: normal; }
  .block-panel { display: flex; flex-direction: column; gap: 8px; }
  .block-panel > h2 { margin: 0; }
  .block-link-card { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; color: var(--fg);
                     background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-panel);
                     text-decoration: none; }
  .block-link-card:hover { border-color: var(--fg); text-decoration: none; }
  .block-link-title { font-weight: 600; }
  .block-link-line { font-size: 12px; }
  .block-link-host { font-size: 11px; font-family: var(--font-mono); overflow-wrap: anywhere; }
  .block-figure { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; margin: 0; }
  .block-image { max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--radius-panel); }
  .block-thumb { display: inline-flex; }
  .block-thumb .thumb { width: 160px; height: 160px; }
  .block-figure figcaption { font-size: 12px; }
  /* The editor's inputs run the full width of the slot: `.field`'s 480px cap is a FORM PAGE's
     measure, and a block's editor is as wide as the panel it will become. */
  .block-editor { display: grid; gap: 8px; }
  .block-editor .field { margin-bottom: 0; }
  .block-editor .field input, .block-editor .field textarea { max-width: none; }
  .block-editor-actions { display: flex; flex-wrap: wrap; gap: 8px; }
  .block-preview { padding: 8px 10px; border: 1px dashed var(--line-strong); border-radius: var(--radius-panel); }
  .block-missing { margin: 0; font-size: 12px; color: var(--danger); }
  .image-picker { display: flex; flex-wrap: wrap; gap: 8px; }
  .image-pick { display: flex; flex-direction: column; gap: 4px; width: 96px; padding: 4px; color: var(--fg);
                font: 12px/1.4 var(--font-ui); background: none; border: 1px solid transparent;
                border-radius: var(--radius-panel); cursor: pointer; }
  .image-pick:hover { border-color: var(--line-strong); }
  .image-pick.active { border-color: var(--fg); background: var(--raised); }
  .image-pick .thumb { width: 100%; height: 72px; }
  .image-pick-name { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
  .page-intro { display: grid; gap: 12px; margin: 0 0 12px; }
  .page-intro-cover { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
  .cover-current { font-size: 12px; color: var(--fg-muted); }
  .page-intro-description { margin-bottom: 0; }
  .page-intro-description textarea { max-width: none; }
  @media (min-width: 900px) { .page-intro { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
  /* MONO MEANS DATA (decision 4): paths, hashes, sizes, timestamps, quantities, IDs. A group head
     is a LABEL, and mono was doing so much chrome work that it had stopped distinguishing anything
     — 22 mono leaf elements on the project page, 20 on inventory. Uppercase and the letter-spacing
     stay: they are what makes this read as a group head, and they do that in either face. */
  .attachment-group-head, .tasks-group h3, .group-head {
    margin: 0 0 var(--space-2); font: 600 11px/16px var(--font-ui); text-transform: uppercase; letter-spacing: .08em; color: var(--fg-muted);
  }
  .attachment-group-head { cursor: pointer; }
  .attachment-group-head:hover { color: var(--fg); }
  table.files, .table { width: 100%; border-collapse: collapse; font-size: 14px; }
  table.files th, .table th {
    text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line-strong); white-space: nowrap;
    font: 600 11px/16px var(--font-ui); text-transform: uppercase; letter-spacing: .08em; color: var(--fg-muted);
  }
  table.files td, .table td { height: var(--row-height); padding: 6px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
  /* THE LIST'S ROW DIVIDER IS THE ROW'S, NOT ITS CELLS' (slice 7). `td.row-actions` is
     `display: flex` (the .row-actions component), which makes it a flex box inside an anonymous
     table cell rather than a cell: its border left the collapsed row line, sat at the bottom of
     a 44px box in a taller row, and faded with the actions' opacity below, so the divider broke
     under the actions column on the Library and the Trash. A row's border in a
     `border-collapse: collapse` table runs under every cell, anonymous ones included.
     test/system/files_layout_test.rb samples the painted pixels. */
  table.files tbody tr { border-bottom: 1px solid var(--line); }
  table.files tbody td { border-bottom: 0; }
  table.files tbody tr:hover, .table tbody tr:hover { background: var(--raised); }
  table.files th a { color: inherit; }
  table.files th a:hover { text-decoration: underline; }
  table.files th.sorted a { color: var(--fg); }
  table.files .num, .table .num { text-align: right; font: 400 12px/16px var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--fg-muted); }
  table.files .row-actions { justify-content: flex-end; opacity: 0; transition: opacity .12s; }
  table.files tr:hover .row-actions, table.files tr:focus-within .row-actions { opacity: 1; }
  @media (hover: none) { table.files .row-actions { opacity: 1; } }
  .dir { font-weight: 600; }
  .file-cell { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
  .file-name { overflow-wrap: anywhere; }

  /* --- Tiles and thumbnails (the markup is files/_thumb.html.erb; thumbs_controller swaps its
     children, so every state below keeps its class) --- */
  /* INK ON PAPER (design-system.md rule 5). Every render keeps a pure-white ground, and MULTIPLY
     makes that white the card while ink stays ink, so our renders, LightBurn's own embedded
     thumbnails and a PDF's page 1 all land on one ground in both themes without a stored pixel
     being edited. `isolation` keeps the blend inside the tile: without it the image multiplies
     with whatever is behind the tile, not with the card. A photo opts out below. */
  .thumb {
    position: relative; display: flex; align-items: center; justify-content: center; overflow: hidden;
    border: 1px solid var(--line); border-radius: var(--radius-control); background: var(--thumb-stage); color: var(--thumb-ink);
    isolation: isolate;
  }
  .thumb img { width: 100%; height: 100%; object-fit: contain; display: block; mix-blend-mode: multiply; }
  .thumb-icon { font: 500 11px/1 var(--font-mono); letter-spacing: .06em; color: var(--thumb-ink); }
  .thumb-spinner {
    width: 14px; height: 14px; border: 2px solid transparent; border-top-color: var(--thumb-ink); border-right-color: var(--thumb-ink);
    border-radius: 50%;
  }
  /* A SPINNER TURNS ONLY WHERE THE POLL WILL ANSWER IT: inside a cell thumbs_controller.js reads
     (`cellTargets`, where "a cell holding a spinner" IS its pending predicate). Anywhere else, on
     every public surface (/files/thumbnails is session-authed) and under any mount with no cell,
     nothing will replace it, so it is a still ring: the render is queued, it will not arrive in this
     tab. Keyed on the poll's own markup, so no page sheet lists the containers that do not poll —
     pages/public.css kept that list and missed two (F-S34-1). test/system/thumb_spinner_test.rb. */
  [data-thumbs-target~="cell"] .thumb-spinner { animation: thumb-spin .8s linear infinite; }
  @keyframes thumb-spin { to { transform: rotate(360deg); } }
  table.files .thumb { width: 32px; height: 32px; flex: 0 0 32px; }
  .folder-thumb, .generic-thumb { background: var(--raised); color: var(--fg-muted); }
  .folder-thumb .folder-glyph { width: 40%; height: 40%; }
  .generic-thumb { font: 500 12px/1 var(--font-mono); letter-spacing: .08em; }
  /* A user's PHOTO on a tile: the neutral matte and a crop, never letterboxed on the card. NOT
     multiplied: a photo is not ink on paper, and Graphite's matte is near-black, so a multiplied
     photo would be a black square. */
  .thumb.photo { background: var(--photo-stage); }
  .thumb.photo img { object-fit: cover; mix-blend-mode: normal; }
  /* THE SAME CARD AT A VIEWER'S SIZE: a file page's poster, the public hero's render. The `.thumb`
     rule above, for an image that is not a tile. Spans its column and centres the picture, so a
     poster occupies the footprint its viewer opens into. */
  .render-card {
    display: flex; align-items: center; justify-content: center; overflow: hidden;
    border: 1px solid var(--line); border-radius: var(--radius-panel); background: var(--thumb-stage);
    isolation: isolate;
  }
  .render-card img { display: block; max-width: 100%; height: auto; mix-blend-mode: multiply; }
  /* A FAILED RENDER KEEPS ITS TYPE ICON AND GAINS A MARK, so "no preview for this type" and "the
     render failed" stop looking identical (F onboarding). thumbs_controller.js adds the class, with
     the icon's title, when the poll answers `failed`; an unsupported file gets neither. A STATE OF
     THE COMPONENT, so it lives here and not in a page sheet: the poll runs on Recent, Projects, the
     project page and the model pages as well as the Library. The mark is a glyph, not colour alone
     (design-system.md rule 13), painted in the stage's own pair, `--thumb-ink` and `--thumb-stage`,
     which flip together (rule 5), so it keeps its contrast on the card in both themes. The second `content` hides the
     glyph from a screen reader where the alt syntax is understood; the title carries the words. */
  .thumb.thumb-failed::after {
    content: "!"; content: "!" / "";
    position: absolute; top: 3px; right: 3px; display: grid; place-items: center; width: 14px; height: 14px;
    border-radius: var(--radius-pill); background: var(--thumb-ink); color: var(--thumb-stage); font: 700 10px/1 var(--font-ui);
  }
  /* The folder glyph (ShellHelper#folder_glyph): the rail's `files` icon, inline beside a name. */
  .folder-glyph, .file-glyph { width: 16px; height: 16px; flex: 0 0 auto; vertical-align: -3px; }
  .filechooser-row .folder-glyph, .filechooser-row .file-glyph { margin-right: 6px; }
  .grid-card { display: flex; flex-direction: column; gap: 2px; min-width: 0; color: var(--fg); text-decoration: none; }
  .grid-card .thumb { margin-bottom: var(--space-2); transition: border-color .12s; }
  .grid-card:hover { text-decoration: none; }
  .grid-card:hover .thumb { border-color: var(--fg); }
  .grid-card .grid-name { font-size: 13px; line-height: 18px; overflow-wrap: anywhere; }

  /* --- Metadata strip, badges, empty state --- */
  dl.meta {
    display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px var(--space-4); margin: 0 0 var(--space-4);
    padding: 12px var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-panel); background: var(--surface);
  }
  /* `dt` is the label and `dd` is the value, which is decision 4's split inside one component:
     the term goes to the UI face, the data keeps mono. */
  dl.meta dt { font: 600 11px/20px var(--font-ui); text-transform: uppercase; letter-spacing: .08em; color: var(--fg-muted); }
  dl.meta dd { margin: 0; font: 400 13px/20px var(--font-mono); overflow-wrap: anywhere; }
  @media (min-width: 900px) { dl.meta { grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr); } }
  /* ONE PILL VOCABULARY (decision 5). `.chip`, `.tag-chip` and `.badge` are three components
     saying the same kinds of thing, and until 2026-09-10 they said them in two typefaces and three
     capitalisations: a 12px MONO chip, an 11px MONO lowercase tag, an 11px sans CAPITALIZED badge.
     On /projects the same taxonomy rendered twice 200px apart — "Laser cutting" in mono, "Laser
     Cutting" in sans. All three now share `--font-ui` (decision 4: mono means DATA — paths,
     hashes, sizes, timestamps, IDs — not labels).

     `text-transform: capitalize` IS GONE AND MUST NOT COME BACK. It cannot know that
     `want_to_print` reads "Wanted", so it rendered "Want To Print" beside a filter chip saying
     "Wanted" — one state, two words, one screen. Labels come from a hash keyed on the enum
     (Maker::Model::PRINT_STATUS_LABELS, Maker::ProjectKind::LABELS) so both callers read one
     source. `.tag-chip` KEEPS `lowercase`: a tag is user-entered and lowercasing it is a real
     normalisation, not a display trick. Since the 2026-10-08 redesign a `.badge` is
     square-shouldered (`--radius-control`) where a `.chip` stays round: shape says
     fixed-versus-removable, face and case stay shared. */
  .badge {
    display: inline-flex; align-items: center; padding: 1px 7px; border-radius: var(--radius-control);
    font: 600 12px/18px var(--font-ui); background: var(--raised); color: var(--fg-muted); white-space: nowrap;
  }
  .badge.status-archived { background: var(--raised); color: var(--fg-muted); }
  /* --- Project status and the new-project mark (stage 4, S4-6). Variants of `.badge`, so they
     live HERE and nowhere else: a component selector is defined in components.css alone
     (design-system rule 3), and `pages/projects.css` may qualify one inside a container but never
     redeclare it. `planning` has no rule because it is the default and is never badged. --- */
  .badge.status-active { background: var(--info-soft); color: var(--info); }
  .badge.status-paused { background: var(--raised); color: var(--fg-muted); }
  .badge.status-done { background: var(--ok-soft); color: var(--ok); }
  .badge.project-new { background: var(--info-soft); color: var(--info); text-transform: none; }
  .badge.mstatus-want_to_print { background: var(--info-soft); color: var(--info); }
  .badge.mstatus-printed { background: var(--ok-soft); color: var(--ok); }
  .badge.mstatus-failed { background: var(--danger-soft); color: var(--danger); }
  .badge.low-stock { background: var(--warn-soft); color: var(--warn); text-transform: none; }
  .badge.bom-stock { background: var(--ok-soft); color: var(--ok); text-transform: none; }
  .badge.bom-file { background: var(--info-soft); color: var(--info); text-transform: none; }
  .badge.bom-text { background: var(--raised); color: var(--fg-muted); text-transform: none; }
  /* Publishing (stage 3, S3-4): the project head's visibility badge. Public reads as "on", unlisted
     as a warning tone, since the accent is reserved for the one action -- a link, not a listing.
     Private renders no badge at all. */
  .badge.visibility-public { background: var(--ok-soft); color: var(--ok); }
  .badge.visibility-unlisted { background: var(--warn-soft); color: var(--warn); }
  /* Releases (stage 3, S3-5): a release recorded from a folder larger than the snapshot's cap says
     so on its row -- the quiet neutral, since it is a fact about the record, not a fault. NOT
     `.badge.truncated`: the bare `.truncated` BANNER rule below shares that class and, later in the
     source at equal specificity, turned the chip into a squared box with a stray margin (measured
     in a browser by S3-5's review). */
  .badge.partial { background: var(--raised); color: var(--fg-muted); text-transform: none; }
  /* Releases (stage 3, S3-5): "N changed since" on a release summary, and "changed"/"gone" on a
     file row. A COMPONENT MODIFIER, not a page rule, because both the private panel and the public
     one render it -- a copy in each page sheet is the shape rule 3 (design-system.md) forbids.
     `text-transform: none` for the same reason the bom-* modifiers carry it: these read as words,
     not as a label. */
  .badge.changed { background: var(--info-soft); color: var(--info); text-transform: none; }
  /* ONE EMPTY STATE, TWO SIZES (decision, UI-cohesion spec §2.4). `.empty` had THREE
     implementations before 2026-09-10: this one, plus `.projects-empty` and `.models-empty`, which
     carried these four declarations VERBATIM under new names in two page sheets. That slipped past
     css_hygiene_test.rb because the guard forbids REDECLARING a component selector, not copying
     its declarations under a different name.

     `.empty.inline` is the variant that was missing. The 40px dashed box is right for "this whole
     page has nothing on it" and wrong for a one-line "nothing yet" inside a list or a card — so
     those cases used to take the big box and then override it back down (pages/settings.css had
     `.filing-card .empty { padding: 4px 0; text-align: left; border: 0 }` doing exactly that, and
     it is deleted now). A `<li>` inside a list takes `.empty.inline`.

     `.muted` ON AN `.empty` IS INERT AND WAS WRITTEN 18 TIMES — `color: var(--fg-muted)` is right
     here. All 18 are dropped; do not add a nineteenth. */
  .empty {
    padding: var(--space-7) var(--space-4); text-align: center; color: var(--fg-muted);
    border: 1px dashed var(--line-strong); border-radius: var(--radius-panel); background-color: var(--bg);
    background-image: linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px);
    background-size: 12px 12px; background-position: -1px -1px;
  }
  .empty.inline { padding: 4px 0; text-align: left; border: 0; background: none; font-size: 13px; }
  /* INSIDE A PANEL THE DASH IS DOUBLE FRAMING. The dashed box exists to mark out a region that has
     nothing in it on a bare page; a `.panel` already draws that region, so the two together read as
     a box inside a box. Found by looking at Home after its four sections became panels (slice 4) —
     the markup did not change, and neither did `.empty`, but putting one inside the other did. */
  .panel .empty { border: 0; padding: 24px 16px; background: none; }
  /* An .empty that HOLDS elements rather than being one — /tools first-run state. `p` carries a
     bottom margin, which would otherwise sit inside the box's own padding and make it lopsided. */
  .empty > :last-child { margin-bottom: 0; }
  .truncated { margin: 0 0 var(--space-3); padding: 8px 12px; border-radius: var(--radius-control); background: var(--warn-soft); color: var(--fg); font-size: 13px; }

  /* --- Flash (inline colours here; the floating stack is shell.css's .flash-stack) --- */
  .flash { display: flex; align-items: flex-start; gap: var(--space-3); padding: 10px 12px; border-radius: var(--radius-float); border: 1px solid; font-size: 14px; color: var(--fg); background: var(--surface); }
  /* Each toast LEADS WITH ITS WORD: a status is never colour alone (design-system.md rule 13). */
  .flash::before { flex: 0 0 auto; font: 600 11px/21px var(--font-ui); letter-spacing: .08em; text-transform: uppercase; }
  .flash-notice { border-color: var(--ok); background: var(--ok-soft); }
  .flash.flash-notice::before { content: "Done"; color: var(--ok); }
  .flash-alert { border-color: var(--danger); background: var(--danger-soft); }
  .flash.flash-alert::before { content: "Problem"; color: var(--danger); }
  .flash-text { flex: 1; }
  .flash-close { background: none; border: 0; padding: 0 2px; color: inherit; font-size: 16px; line-height: 1; cursor: pointer; opacity: .7; }
  .flash-close:hover { opacity: 1; }

  /* --- Floating things: menu, popover, dialog, composer --- */
  .file-menu {
    position: absolute; z-index: 1000; min-width: 200px; padding: 4px;
    background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-float); box-shadow: var(--shadow-float);
  }
  .file-menu button, .file-menu a {
    display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: var(--control-height); padding: 0 10px;
    text-align: left; background: none; border: 0; color: var(--fg); font: 400 14px/1 var(--font-ui);
    border-radius: var(--radius-control); cursor: pointer; text-decoration: none; box-sizing: border-box;
  }
  .file-menu button:hover, .file-menu a:hover { background: var(--raised); text-decoration: none; }
  /* THE ONE DESTRUCTIVE ENTRY READS AS ONE. filemenu_controller.js sets `className = "danger"` on
     Delete; with no rule for it that class styles NOTHING, and the item that removes a file would
     look like one more navigation. `.btn.danger` cannot be reused - a menu entry is not a button
     face - so this is the same token, not a second definition of it. */
  .file-menu button.danger { color: var(--danger); }
  .note-popover {
    position: absolute; z-index: 1001; width: 320px; max-width: 90vw; padding: 10px;
    background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-float); box-shadow: var(--shadow-float);
  }
  .note-popover textarea { width: 100%; box-sizing: border-box; resize: vertical; }
  .note-popover-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
  .note-popover-status { font-size: 12px; color: var(--fg-muted); }
  .note-badge { display: inline-block; margin-left: 6px; font: 500 11px/1 var(--font-mono); color: var(--fg-muted); vertical-align: middle; white-space: nowrap; }
  dialog.move-conflict {
    display: block; position: static; width: 100%; max-width: none; margin: 0 0 16px; padding: 12px 14px;
    border: 1px solid var(--line-strong); border-radius: var(--radius-panel);
    background: var(--surface); color: var(--fg);
  }
  dialog.move-conflict p { margin: 0 0 10px; }
  dialog.move-conflict .dialog-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  dialog.move-conflict form { display: inline; }
  details.composer > summary { list-style: none; }
  details.composer > summary::-webkit-details-marker { display: none; }
  details.composer[open] > summary { margin-bottom: 10px; }

  /* --- Overflow menu: a <details> the browser opens; menu_controller.js closes it on an
     outside click. The summary is a .btn (the caller adds .icon for the ⋯ form). --- */
  details.menu { position: relative; display: inline-block; }
  details.menu > summary { list-style: none; }
  details.menu > summary::-webkit-details-marker { display: none; }
  details.menu[open] > summary { border-color: var(--line-strong); }
  .menu-list {
    position: absolute; right: 0; top: calc(100% + 4px); z-index: 50; min-width: 220px; padding: 4px;
    background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-float); box-shadow: var(--shadow-float);
  }
  .menu-list form { margin: 0; }
  .menu-item {
    display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: var(--control-height); padding: 0 10px;
    text-align: left; background: none; border: 0; color: var(--fg); font: 400 14px/1 var(--font-ui);
    border-radius: var(--radius-control); cursor: pointer; text-decoration: none; white-space: nowrap;
  }
  .menu-sep { height: 1px; margin: 4px 6px; background: var(--line); }
  /* An inline form inside a menu (New ▸ Folder): a label, then a field and its button on one row. */
  .menu-form { display: grid; gap: var(--space-1); padding: 6px 10px 8px; margin: 0; }
  .menu-form-label { font-size: 13px; font-weight: 500; color: var(--fg); }
  .menu-form-row { display: flex; gap: var(--space-2); }
  .menu-form-row input[type="text"] { flex: 1; min-width: 0; }
  .menu-item:hover { background: var(--raised); text-decoration: none; }
  .menu-item.danger { color: var(--danger); }
  /* A menu row that HOSTS a control rather than being one — the build-log uploader is a label
     wrapping a hidden file input, so it cannot be a .menu-item anchor without losing the input. */
  .menu-item--host { padding: 4px 6px; }
  .menu-item--host .upload-btn span { width: 100%; justify-content: flex-start; border: 0; background: none; }

  /* --- The folder/file chooser (filechooser_controller.js; shared with the addon bundles, which
     build this markup themselves — every class here is part of the §14c contract) --- */
  .folder-field { display: inline-flex; }
  .folder-field-btn {
    display: inline-flex; align-items: center; gap: 6px; max-width: 260px; min-height: var(--control-height);
    padding: 0 12px; border: 1px solid var(--line-strong); border-radius: var(--radius-control);
    background: var(--surface); color: var(--fg); font: 400 14px/20px var(--font-ui); cursor: pointer;
  }
  .folder-field-btn:hover { border-color: var(--fg-muted); }
  .folder-field-btn .ff-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .filechooser-overlay {
    position: fixed; inset: 0; z-index: 1000; padding: 16px; background: var(--scrim);
    display: flex; align-items: center; justify-content: center;
  }
  .filechooser {
    display: flex; flex-direction: column; overflow: hidden; width: min(720px, 100%); max-height: min(560px, 90vh);
    background: var(--surface); color: var(--fg); border: 1px solid var(--line); border-radius: var(--radius-float); box-shadow: var(--shadow-float);
  }
  .filechooser-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--line); font-weight: 600; }
  .filechooser-close { background: none; border: 0; font-size: 16px; color: var(--fg-muted); cursor: pointer; }
  .filechooser-crumbs { padding: 8px 16px; border-bottom: 1px solid var(--line); color: var(--fg-muted); font: 12px/1.4 var(--font-mono); white-space: nowrap; overflow-x: auto; }
  .filechooser-cols { display: flex; flex: 1; min-height: 0; overflow-x: auto; }
  .filechooser-col { flex: 0 0 200px; min-height: 0; overflow-y: auto; border-right: 1px solid var(--line); }
  /* THE INLINE VARIANT (mode "browse", mounted by filebrowser_controller.js into a repository's
     section on the project page). It only undoes the modal's POSITIONING — a fixed height in the
     flow instead of a centred panel capped at the viewport — so every column, row, empty-state and
     the narrow-width rule at the end of this block go on applying unchanged. A separate
     `.repo-browser` component would have been a second definition of all of them. */
  .filechooser--inline { position: static; width: 100%; max-height: none; height: 320px; border: 1px solid var(--line); border-radius: var(--radius-panel); }
  .filechooser-bar { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--line); }
  /* The crumbs keep their font and take the rest of the bar; the bar already draws the divider, so
     the crumbs' own bottom border would double it. */
  .filechooser-bar .filechooser-crumbs { flex: 1; min-width: 0; padding: 0; border-bottom: 0; }
  /* A ROW IS A `div` IN THE PICKING MODES AND A `button`/`a` IN BROWSE MODE — real controls, so
     middle-click and the keyboard work on a file link — which means this rule has to flatten a
     UA-styled button and an anchor back to what the `div` row always looked like. */
  .filechooser-row { display: block; padding: 7px 12px; font-size: 13px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; appearance: none; border: 0; background: none; width: 100%; text-align: left; font-family: inherit; color: inherit; text-decoration: none; }
  .filechooser-row:hover { background: var(--raised); }
  .filechooser-row.sel { background: var(--fg); color: var(--surface); }
  .filechooser-row.is-file { color: var(--fg-muted); }
  .filechooser-row.sel.is-file { color: var(--surface); }
  .filechooser-empty { padding: 10px 12px; color: var(--fg-muted); font-size: 12px; }
  .filechooser-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 16px; border-top: 1px solid var(--line); }
  .filechooser-actions { display: flex; align-items: center; gap: 8px; }
  /* ON A PHONE THE FOOT WRAPS (redesign slice 7). One row inside the panel's `overflow: hidden` put
     Cancel and Choose past its right edge at 375px, so an upload started from a page with no folder
     could not be finished there. test/system/phone_topbar_test.rb clicks Choose at 375px. */
  @media (max-width: 640px) {
    .filechooser-foot { flex-wrap: wrap; }
    .filechooser-actions { flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
  }
  .filechooser-sel { max-width: 240px; font-size: 12px; color: var(--fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* BACK IS VISIBLE AT EVERY WIDTH, ON PURPOSE. The pre-2026-09 sheets hid it here and showed it
     below 640px — but the button also carries `.btn`, and projects.css's later `.btn` definition
     outranked this file's `display: none`, so Back has always rendered at desktop width, and
     test/system/file_chooser_test.rb pins that (a Back that pops the last column is useful in a
     column view at any width). Keeping the shipped behaviour rather than the comment's intent. */
  .filechooser-newform { display: flex; align-items: center; gap: 6px; }
  .filechooser-newerr { color: var(--danger); font-size: 12px; }
  @media (max-width: 640px) {
    .filechooser-col { flex-basis: 100%; }
    .filechooser-col:not(:last-child) { display: none; }
  }

  /* --- Shell: the skip link and the unreachable-library banner (F onboarding, 2026-10-13) --- */
  /* THE SKIP LINK is the first element in <body> (layouts/application.html.erb) and jumps to
     main#main. ABSOLUTELY POSITIONED, so it takes no cell of body.app-shell's grid and the rail
     keeps the first column; translated above the viewport until it has focus. `:focus` rather than
     `:focus-visible`, so it shows for any focus that lands on it. */
  .skip-link {
    position: absolute; top: var(--space-2); left: var(--space-2); z-index: 100;
    padding: var(--space-2) var(--space-3); border: 1px solid var(--line-strong); border-radius: var(--radius-control);
    background: var(--surface); color: var(--fg); font: 500 14px/20px var(--font-ui); white-space: nowrap;
    transform: translateY(-200%);
  }
  .skip-link:focus { transform: none; text-decoration: none; }
  /* THE BANNER (layouts/_library_unreachable) sits INLINE at the top of main. It is not a `.flash`:
     only the flash stack floats (rule 10). It leads with its words in <strong>, so the warn tone is
     never the only signal (rule 13). */
  .library-unreachable {
    margin: 0 0 var(--space-4); padding: 10px 12px; border: 1px solid var(--warn); border-radius: var(--radius-panel);
    background: var(--warn-soft); color: var(--fg); font-size: 14px; overflow-wrap: anywhere;
  }
  /* THE DEMO BANNER (layouts/_demo_banner, H 2026-10-14): the same inline box in the INFO tone, because
     it states a fact about the account rather than a fault -- and when both show, the warning must
     still read as the different thing. Leads with its words in <strong>, like the one above. */
  .demo-banner {
    margin: 0 0 var(--space-4); padding: 10px 12px; border: 1px solid var(--info); border-radius: var(--radius-panel);
    background: var(--info-soft); color: var(--fg); font-size: 14px; overflow-wrap: anywhere;
  }
/* --- The calculator (layouts/_calculator.html.erb, calculator_controller.js; 2026-10-21). A
   native <dialog> opened with showModal(), so it sits in the top layer above every page and its
   ::backdrop is the scrim; the UA's own dialog border, padding and colours are reset here. At
   phone width it takes the whole width less the gutter and scrolls inside itself, so nothing on
   a 375px screen scrolls sideways (design-system.md rule 11). --- */
dialog.calculator {
  width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); padding: 0; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--radius-float); box-shadow: var(--shadow-float);
  background: var(--surface); color: var(--fg); font: 400 14px/1.4 var(--font-ui);
}
dialog.calculator::backdrop { background: var(--scrim); }
.calculator-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--line); }
.calculator-head h2 { margin: 0; font-size: 15px; font-weight: 600; }
.calculator-close { min-width: var(--control-height-small); min-height: var(--control-height-small); background: none; border: 0; border-radius: var(--radius-control); color: var(--fg-muted); font-size: 20px; line-height: 1; cursor: pointer; }
.calculator-close:hover { background: var(--raised); color: var(--fg); }
.calculator-entry { padding: 14px 16px 0; }
.calculator-input-label { display: block; margin-bottom: 4px; font-size: 12px; color: var(--fg-muted); }
.calculator-input-row { display: flex; gap: var(--space-2); }
.calculator-input-row .calculator-input { flex: 1; min-width: 0; font: 400 16px/22px var(--font-mono); }
.calculator-equals { min-width: 44px; font-size: 18px; }
.calculator-answer { padding: 10px 16px 4px; min-height: 52px; overflow-wrap: anywhere; }
.calculator-result { display: block; font: 600 22px/1.3 var(--font-mono); color: var(--fg); }
.calculator-result.stale { color: var(--fg-muted); }
.calculator-result-alt { display: block; font: 400 13px/1.4 var(--font-mono); }
.calculator-error { margin: 0; padding: 6px 10px; border-radius: var(--radius-control); background: var(--danger-soft); color: var(--danger); font-size: 13px; }
.calculator-fraction { display: flex; align-items: center; gap: var(--space-2); padding: 4px 16px 10px; font-size: 12px; }
.calculator-section { margin: 0 16px; border-top: 1px solid var(--line); }
.calculator-section > summary { padding: 10px 0; font-weight: 500; cursor: pointer; }
.calculator-section[open] > summary { padding-bottom: 6px; }
.calculator-section[open] { padding-bottom: 12px; }
.calculator-history { list-style: none; margin: 0 0 8px; padding: 0; max-height: 180px; overflow-y: auto; }
.calculator-history-item { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; width: 100%; padding: 6px 8px; border: 0; border-radius: var(--radius-control); background: none; color: var(--fg); font: 400 13px/1.4 var(--font-mono); text-align: left; cursor: pointer; overflow-wrap: anywhere; }
.calculator-history-item:hover { background: var(--raised); }
.calculator-history-item code { font: inherit; }
.calculator-history-result { color: var(--fg-muted); }
.calculator-save-fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-2); }
.calculator-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; font-size: 12px; color: var(--fg-muted); }
.calculator-field select, .calculator-field input { width: 100%; min-width: 0; box-sizing: border-box; }
.calculator-save-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin-top: var(--space-2); }
.calculator-save-what { font: 400 12px/1.4 var(--font-mono); overflow-wrap: anywhere; }
.calculator-saved { margin: 0 0 var(--space-2); padding: 6px 10px; border-radius: var(--radius-control); background: var(--ok-soft); color: var(--fg); font-size: 13px; }
.calculator-save-error { margin: 0 0 var(--space-2); padding: 6px 10px; border-radius: var(--radius-control); background: var(--danger-soft); color: var(--danger); font-size: 13px; }
.calculator-save-hint { margin: 0 0 var(--space-2); font-size: 13px; }
.calculator-help { margin: 0; padding: 10px 16px 14px; border-top: 1px solid var(--line); font-size: 12px; line-height: 1.6; }
.calculator-help code { font: 400 12px/1 var(--font-mono); white-space: nowrap; }
@media (max-width: 480px) {
  .calculator-save-fields { grid-template-columns: minmax(0, 1fr); }
}

  /* The demo's door on the sign-in card (users/sessions/new): its button and one sentence, set off
     from the form below by the hairline the card's links use above them. */
  .demo-entry { margin: 0 0 var(--space-4); padding: 0 0 var(--space-4); border-bottom: 1px solid var(--line); }
  .demo-entry .auth-hint { margin: var(--space-2) 0 0; }
}
