/* The Projects hub: index tiles, the project page's panels, tasks, the build-log timeline,
   sheets. Panels, buttons, badges and fields are components. */
@layer pages {
  /* THE BUILD-LOG COMPOSER SITS IN THE PANEL HEAD, and open it takes the whole row. Without this it
     is a flex item beside the title and the editor renders about 70px wide — measured, in a
     screenshot, after the reasoning that predicted a graceful wrap turned out to be wrong.
     Qualified inside  because only this head holds a disclosure. */
  .build-log-panel .panel-head > details.composer[open] { flex-basis: 100%; }
  /* --- Index --- */
  .projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 16px; }
  /* THE MODELS PANEL (2026-10-21): a model card with its Detach under it, centred. */
  .models-panel .project-model { display: flex; flex-direction: column; gap: 6px; }
  .models-panel .project-model > form { align-self: center; }
  /* Deliberately NOT `align-items: center` here: `.project-card` is a flex item of `.project-tile`
     with no explicit width, so `align-items: center` sizes it to shrink-to-fit (its content's own
     width) instead of stretching it — which starves `.project-card .thumb`'s `width: 100%` down to
     the thumb's own content size (measured: a 161.5px tile shrank the thumb to ~50px). Leaving
     `align-items` at its flex default (`stretch`) keeps `.project-card` — and so its `.thumb` — at
     the tile's full width, exactly as the plain-block wrapper this replaces did. The Unarchive
     button is centered instead by scoping `text-align: center` to the form alone, which centers its
     inline-block `<button>` without touching `.project-card`'s box model at all. */
  .project-tile { display: flex; flex-direction: column; gap: 6px; }
  .project-tile form { text-align: center; }
  .project-tile > .badge { align-self: center; }
  .project-card { display: flex; flex-direction: column; gap: 8px; align-items: center; color: var(--fg); }
  .project-card:hover { text-decoration: none; }
  .project-card:hover .thumb { border-color: var(--fg); }
  .project-card .thumb { width: 100%; height: 120px; }
  /* An empty cover is the rail's projects icon (slice 6; no emoji in the interface), at the size the
     glyph it replaced was set. `.project-card` qualifies it, so the public explore card (which is a
     `.project-card` too) and the model card (`.project-card.model-card`) draw it the same size. */
  .project-card .project-thumb-empty .rail-icon { width: 36px; height: 36px; color: var(--fg-muted); }
  .project-name { font-size: 13px; font-weight: 600; text-align: center; overflow-wrap: anywhere; }
  .archived-note { margin: 24px 0 0; padding-top: 12px; border-top: 1px solid var(--line); font-size: 12px; }

  /* --- Show: head, stats, panels --- */
  .project-head { align-items: flex-start; }
  /* THE ACTIONS KEEP TO THE RIGHT EVEN WHEN THE HEAD WRAPS (2026-10-26, the edit area). "+ Add panel"
     is the last of them since the ⋯ menu went behind Edit, and its list hangs from the summary's RIGHT
     edge (components.css `.menu-list`): wrapped to the left of a 375px page, the list started 61px off
     the page (shell_test.rb measured it). `margin-left: auto` puts a wrapped row at the right, where
     the ⋯ menu used to push it. */
  .project-head .head-actions { margin-left: auto; }
  /* THE HUB'S HEAD (structural redesign §6, slice 6): the cover, then a text column holding the
     PROJECT eyebrow on its own line and the name, badges and links wrapping after it. Two boxes side
     by side — never wrapping, so the cover stays beside the name at 375px — and `min-width: 0` on
     both, so a long name wraps inside the column instead of widening the page. */
  .project-head .page-title { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
  .project-head-text { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; min-width: 0; }
  .project-head-text .eyebrow { flex-basis: 100%; font: 600 11px/16px var(--font-ui); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-muted); }
  .project-head-text h1 { min-width: 0; overflow-wrap: anywhere; }
  .project-description { white-space: pre-wrap; margin: 0 0 12px; max-width: 72ch; font-size: 14px; line-height: 1.55; }
  .project-stats { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 0 0 20px; font: 12px/1.5 var(--font-mono); color: var(--fg-muted); }
  .project-stats b { color: var(--fg); font-weight: 500; }
  /* --- The hero (stage 4, S4-1): the cover on the project's own page. An image cover is the
     picture, capped like the model page's; any other cover is its thumbnail in a poll cell. --- */
  .project-hero { display: flex; flex-direction: column; gap: 6px; margin: 0 0 20px; }
  .project-hero-img { display: block; width: 100%; max-height: 50vh; object-fit: contain;
                      border: 1px solid var(--line); border-radius: var(--radius-panel); background: var(--thumb-stage); }
  /* The public page's RENDERED cover sits on `.render-card` (components.css), which owns the card,
     the border and the multiply, so the picture inside drops its own: an image painting the card
     colour itself would be multiplied by the card a second time and read darker. */
  .project-hero-render .project-hero-img { border: 0; border-radius: 0; background: none; }
  .project-hero-thumb { display: block; width: min(100%, 480px); }
  .project-hero-thumb .thumb { width: 100%; height: 240px; }
  .project-hero-name { font-size: 12px; }
  /* THE COVER IN THE HEAD (slice 6). The hero partial is unchanged; it is placed and sized here. A
     photo sits on `--photo-stage`, cropped to fill (design-system.md rule 5: a user's photo is not a
     render); a poll cell keeps its `.thumb`'s own card, where a render is multiplied (rule 5), and
     is stretched to the same square. The name below the picture is the old full-width
     hero's caption, and the head already says what the picture is of. */
  .project-head .project-hero { flex: none; width: 96px; height: 96px; margin: 0; gap: 0; overflow: hidden;
                                background: var(--photo-stage); border-radius: var(--radius-control); }
  .project-head .project-hero > a { display: block; width: 100%; height: 100%; }
  .project-head .project-hero-img { width: 100%; height: 100%; max-height: none; object-fit: cover;
                                    border: 0; border-radius: 0; background: var(--photo-stage); }
  .project-head .project-hero-thumb .thumb { width: 100%; height: 100%; }
  .project-head .project-hero-name { display: none; }
  @media (max-width: 719px) { .project-head .project-hero { width: 64px; height: 64px; } }
  /* --- Photos (stage 4, S4-1): the public gallery's grid, on the private page, full width. --- */
  .project-photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
  .project-photos .thumb { width: 100%; height: 110px; }
  /* `min-width: 0` ON THE ITEMS IS AN OVERFLOW FIX, NOT TIDINESS. A grid item's automatic minimum
     size is its content's MIN-CONTENT width, so one descendant that cannot wrap sets the width of
     the whole column — and `1fr` does not stop it, because `1fr` is `minmax(auto, 1fr)`. The
     README editor is exactly that descendant: CodeMirror's `.cm-content` is `white-space: pre`, so
     a 140-character line made `.attachments-panel` 1192px wide inside a 375px viewport and the
     PAGE scrolled sideways (measured; test/system/shell_test.rb's phone sweep opens the editor for
     this reason, and nothing lower in the chain is the lever — probed on the live page, only the
     grid item's own min-width moves it). Zeroing it hands the overflow to `.cm-scroller`, which has
     its own `overflow-x: auto` and is where a long line belongs. */
  .project-panels { display: grid; grid-template-columns: 1fr; gap: 16px; }
  /* THE GRID ITEM IS THE SLOT (2026-09-09, the page editor): projects/_panel_slot wraps every panel
     in `div.panel-slot.panel--<width>`, and the width is the LAYOUT's answer (Maker::ProjectKind
     ::Layout#width_for) rather than this sheet naming three panel classes. `.panel` stays in the
     rule for the public page's repo cards, which render outside a slot. */
  .project-panels > .panel, .project-panels > .panel-slot { min-width: 0; }
  /* A SHELF GROUP'S HEADING (2026-10-20, ProjectsHelper#panels_with_group_heads) spans the grid
     above its panels, on one column and two. It is `.group-head`, the component, qualified by its
     place in the grid; masonry_controller.js spans it like any other child. */
  .project-panels > .shelf-group-head { grid-column: 1 / -1; margin: 8px 0 0; }
  @media (min-width: 900px) {
    .project-panels { grid-template-columns: 1fr 1fr; }
    .project-panels > .panel-slot.panel--full { grid-column: 1 / -1; }
    /* NO GAPS (2026-09-09, content blocks; spec §7). masonry_controller.js adds `.masonry` and
       gives every child a row span of its measured height plus the gap, so a short half-width
       panel's neighbour below starts right under it instead of waiting for the tall column.
       THREE DECLARATIONS AND NOTHING ELSE, and each is load-bearing: one-pixel rows are the unit
       the spans count in; the row gap goes to zero because the spans already include it; and
       `align-items: start` is what stops a child stretching to fill its span — without it the
       stretched height would feed the next measurement and every panel would grow without end.
       The rule lives INSIDE the breakpoint, so below it the controller clears the class and the
       grid is exactly today's single column. */
    .project-panels.masonry { grid-auto-rows: 1px; row-gap: 0; align-items: start; }
  }

  /* --- Folder panels (2026-09-08): one per depth-one subfolder of the project's folder, each with
     its own view. Full width by default — Maker::ProjectKind.default_width, since the page editor.

     THE VIEW TOGGLE IS THREE `button_to` FORMS, NOT LINKS, because choosing a view is a WRITE now.
     Two consequences the component sheet cannot know about: each button arrives wrapped in its own
     <form>, so the forms are `display: contents` to put the buttons back in `.view-toggle`'s flex
     row; and `.view-toggle .toggle + .toggle`'s divider can never match, because the buttons are
     not siblings in the DOM whatever their boxes do — the divider is redrawn on the FORM's
     adjacency instead. The <button>'s own UA border and background are reset in components.css on
     `.view-toggle .toggle` itself, NOT here: across cascade LAYERS specificity is never consulted
     and `pages` is declared after `components`, so a reset in this sheet flattened the active and
     hover fills whatever its selector (found in review). */
  .project-panels .view-toggle form.button_to { display: contents; }
  .project-panels .view-toggle form.button_to + form.button_to button { border-left: 1px solid var(--line); }
  .folder-panel .panel-path { font-size: 12px; margin-left: 8px; }
  /* Quiet (S4-1): the heading, the badge and one sentence on one line. `.panel--quiet` already
     zeroes the head's margin and hides the `.mono.muted` count; the sentence needs its own. */
  .folder-panel.panel--quiet > p { margin: 0; }
  /* Quiet (2026-10-21), as Tasks is: an empty Models panel is its heading, Add model and "No models
     yet." on one line, so its sentence needs the same reset. */
  .models-panel.panel--quiet > p { margin: 0; }

  /* --- Files panel: 96px tiles, Detach in the corner on hover --- */
  .attachments-panel .panel-path { font-size: 12px; margin-left: 8px; }
  /* "Recently changed" (slice 6): the newest files anywhere under the folder, above the root's own
     groups, in the same tile grid. The heading is a caption, smaller than a group's summary. */
  .recent-files { margin: 0 0 16px; }
  .recent-files-head { margin: 0 0 8px; font: 600 11px/16px var(--font-ui); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-muted); }
  .attachments-grid, .entry-photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 12px; }
  .attachment-tile { position: relative; display: flex; flex-direction: column; gap: 4px; }
  .attachment-tile .thumb { width: 100%; height: 96px; }
  .attachment-tile .grid-name { font-size: 12px; }
  /* A Linked tile's corner holds TWO controls (Detach, Move into project) since S2-2, stacked and
     right-aligned so they cover as little of the thumbnail as two labels can. On a no-pointer
     device the corner is always visible, and two stacked buttons over a 96px thumb hid it
     entirely — so there the controls sit BELOW the tile in a row instead of over it. */
  .tile-meta { position: absolute; top: 4px; right: 4px; display: flex; flex-direction: column; gap: 4px;
               align-items: flex-end; opacity: 0; transition: opacity .12s; }
  .attachment-tile:hover .tile-meta, .attachment-tile:focus-within .tile-meta { opacity: 1; }
  @media (hover: none) {
    .tile-meta { position: static; opacity: 1; flex-direction: row; flex-wrap: wrap; align-items: center; }
  }
  .tile-meta form { margin: 0; }
  .tile-detach, .tile-move { min-height: 22px; padding: 0 6px; font-size: 11px; background: var(--surface); }
  .entry-photo .thumb { width: 100%; height: 80px; }
  .attachment-group { margin-bottom: 16px; }
  .attachment-group:last-child { margin-bottom: 0; }
  /* A <summary>, so it carries the disclosure triangle and is focusable/clickable for free.
     `list-style` is left alone deliberately — the marker IS the affordance that says "this opens". */
  .attachment-group details[open] > .attachment-group-head { margin-bottom: 8px; }
  .attachments-missing { font-size: 12px; margin: 4px 0 0; }
  .photo-attach { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 6px; font-size: 12px; }

  /* --- The folder as a tree (stage 3, S3-1): a subfolder inside a subfolder nests one indent
     in, the badge sits beside the folder name, and the note under Loose reads as a caption, not
     another section. `.attachment-group-head .badge` is a container qualification of the
     components.css selector (design-system rule 3), not a redeclaration. --- */
  .file-tree-section .file-tree-section { margin-left: 18px; border-left: 1px solid var(--line); padding-left: 14px; }
  .attachment-group-head .badge { margin: 0 2px 0 8px; }
  .attachment-group-head .linkbtn.small { margin-left: 8px; font-size: 11px; }
  .loose-note { font-size: 12px; margin: -8px 0 16px; }
  /* The repository browser's host (projects/_repo_browser, filebrowser_controller.js). The panel
     itself is `.filechooser--inline` from components.css — this only spaces it under the card. */
  .repo-browser { margin-top: 8px; }

  /* --- Repository card (stage 3, S3-2): the README rendered, the latest commit, the current tag,
     at the top of a repo section, before its files. The tag badges and the Commits/History links
     inside `.repo-facts` are plain, unqualified `.badge`/`.linkbtn` from components.css — nothing
     here redeclares either. `.markdown` is a generic name on purpose — a public project page will
     want the identical rendering later (spec S3-4) — kept here rather than promoted to
     components.css until that second caller exists. --- */
  .repo-card { display: flex; flex-direction: column; gap: 10px; margin: 0 0 12px; padding: 10px 12px;
               border: 1px solid var(--line); border-radius: var(--radius-panel); background: var(--surface); }
  .repo-facts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; font-size: 12px; }
  .repo-facts .mono { font-family: var(--font-mono); }
  .repo-summary { font-weight: 500; }
  .repo-card .readme > summary { cursor: pointer; font-size: 12px; color: var(--fg-muted); }
  .readme-note { font-size: 11px; margin: 4px 0 0; }
  .markdown { font-size: 13px; line-height: 1.55; max-height: 360px; overflow: auto; margin-top: 6px; }
  .markdown > :first-child { margin-top: 0; }
  .markdown > :last-child { margin-bottom: 0; }
  .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { font-size: 1em; margin: 0.8em 0 0.3em; }
  .markdown h1 { font-size: 1.3em; }
  .markdown h2 { font-size: 1.15em; }
  .markdown pre { padding: 8px 10px; border-radius: var(--radius-control); background: var(--raised); overflow-x: auto; }
  .markdown code { font-family: var(--font-mono); font-size: 0.9em; }
  .markdown img { max-width: 100%; }
  /* A TEXT BLOCK IS THE PAGE'S OWN PROSE, NOT A README PREVIEW (2026-09-09, content blocks): the
     360px cap and its scrollbar are right for a repository's README inside a card and wrong for
     something the maker wrote to be read here. It lives in THIS sheet, not beside the other block
     rules in components.css, and that is not a slip: across cascade LAYERS specificity is never
     consulted and `pages` is declared after `components`, so `.block-panel .markdown` there would
     lose to the bare `.markdown` above however specific it looked (the same trap the folder-panel
     view toggle records). */
  .block-panel .markdown, .block-preview.markdown { max-height: none; overflow: visible; margin-top: 0; }

  /* --- Generated (stage 3, S3-1): the tile keeps its own thumbnail's shape; the meta line below
     it is always visible (unlike a Linked tile's hover corner), because naming the tool and
     revision is the point of the group, not an on-demand detail. --- */
  .generated-tile { display: flex; flex-direction: column; gap: 4px; }
  .generated-meta { display: flex; flex-direction: column; gap: 2px; font-size: 11px; }
  .generated-meta .linkbtn.small { font-size: 11px; }

  /* --- The list view (stage 3, S3-1): `.table` is the component (components.css);
     `.project-files` carries only this page's column drops, the same split table.files/
     pages/files.css makes. Order of loss narrowest-first: section+modified, then size. --- */
  @media (max-width: 719px) { .project-files .col-section, .project-files .col-modified { display: none; } }
  @media (max-width: 479px) { .project-files .col-size { display: none; } }

  /* --- Tasks: a state mark, the transitions as tiny buttons, the composer last --- */
  .tasks-group { margin-top: 10px; }
  .task-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--line); }
  .task-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
  .status-done .task-title { text-decoration: line-through; color: var(--fg-muted); }
  .task-state { width: 14px; height: 14px; flex: 0 0 auto; border-radius: 50%; border: 1.5px solid var(--fg-muted); position: relative; }
  .task-state--doing { background: linear-gradient(90deg, var(--info) 50%, transparent 50%); border-color: var(--info); }
  .task-state--done { background: var(--ok); border-color: var(--ok); }
  .task-state--done::after { content: ""; position: absolute; left: 4px; top: 1px; width: 3px; height: 7px; border: solid var(--bg); border-width: 0 2px 2px 0; transform: rotate(45deg); }
  .task-actions { display: flex; gap: 4px; align-items: center; }
  .task-actions form { margin: 0; }
  .task-move { min-height: 22px; padding: 0 7px; border: 1px solid var(--line); border-radius: var(--radius-control); background: var(--surface); color: var(--fg-muted); font: 500 11px/1 var(--font-ui); cursor: pointer; }
  .task-move:hover { color: var(--fg); border-color: var(--line-strong); }
  .task-delete { min-height: 22px; padding: 0 6px; border: 0; background: none; color: var(--fg-muted); font: 12px/1 var(--font-ui); cursor: pointer; }
  .task-delete:hover { color: var(--danger); }
  .task-form { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
  .task-form input[type="text"] { flex: 1; max-width: none; margin: 0; }
  .task-error { margin: 8px 0 0; }

  /* --- Build log: composer, then the timeline with its dates in a column --- */
  .build-log-panel details.composer { margin: 0 0 8px; }
  .log-timeline { list-style: none; padding: 0; margin: 8px 0 0; }
  .log-entry { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 4px 14px; padding: 10px 0; border-top: 1px solid var(--line); }
  .entry-time { font: 11px/1.6 var(--font-mono); white-space: nowrap; padding-top: 1px; }
  .entry-main { min-width: 0; }
  .entry-head { display: flex; justify-content: flex-end; }
  .entry-delete { font-size: 11px; }
  /* No `white-space: pre-wrap` here any more. It was right when .entry-body held a raw plain-text
     body — it is what made manual newlines visible. Since the Action Text move the element wraps real
     HTML with its own <br>s, and because white-space INHERITS, pre-wrap stopped collapsing the
     whitespace node the content partial emits and put a ~45px blank gap above every entry. */
  .entry-body { margin: 0 0 6px; font-size: 14px; line-height: 1.55; max-width: 72ch; }

  /* --- Bill of materials: badges are components.css; rows, rollup, composer here --- */
  .bom-rev { font-size: 12px; }
  .bom-lines .col-ref { font-family: var(--font-mono); font-size: 12px; }
  .bom-lines .col-supplier, .bom-lines .col-stock { color: var(--fg-muted); }
  .bom-lines tr.short .col-stock { color: var(--warn); }
  .bom-more { font-size: 12px; margin: 6px 0 0; }
  .bom-rollup { font: 12px/1.5 var(--font-mono); color: var(--fg-muted); margin: 8px 0; }
  .composer-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; align-items: end; margin-bottom: 8px; }
  .composer-fields label { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: var(--fg-muted); }
  /* Fields fill their cell and a phone column may shrink below its content (`minmax(0, 1fr)`): the
     BOM composer's item <select> carries part names as long as a sentence, and its min-content
     differs between macOS and the Linux runners — the shape that pushed /inventory/:id sideways
     at 375px (pages/inventory.css's `.adjust-fields` records it). */
  .composer-fields input, .composer-fields select { width: 100%; min-width: 0; }
  .bom-target { display: flex; flex-direction: column; gap: 8px; border: 0; padding: 0; margin: 0 0 8px; }
  .bom-target legend { font-size: 11px; color: var(--fg-muted); padding: 0; }
  .bom-target label { display: flex; gap: 8px; align-items: center; }
  @media (max-width: 719px) {
    .composer-fields { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .bom-lines .col-supplier, .bom-lines .col-ref { display: none; }
  }

  /* --- Wiring (stage 3, S3-3): a row's fields live in table cells but its <form> lives outside
     the table (projects/_wiring.html.erb's own comment says why), so there is no row wrapper to
     hang a flex layout on -- each field is a plain `.table` input at full cell width instead. Drop
     order narrowest-first, matching the BOM table's own two breakpoints just above: Note first
     (least essential once the row is already named by From/To/Wire), then Wire. --- */
  .wiring input[type="text"] { width: 100%; margin: 0; }
  @media (max-width: 719px) { .wiring .col-note { display: none; } }
  @media (max-width: 479px) { .wiring .col-wire { display: none; } }

  /* --- Releases (stage 3, S3-5): a <details> per release whose summary is a wrapping row of
     facts -- version, date, tag, BOM revision, file count, "N changed since". `list-style: none`
     is NOT set: the disclosure triangle is the affordance, and `display: flex` on a <summary>
     removes it in Chrome, so the flex row lives on an inner rule-free summary instead. Column
     drops narrowest-first, matching the BOM and wiring tables above: Size at 719px (least useful
     once the file is named), State at 479px -- READABILITY, not overflow safety, the same honest
     claim pages/public.css makes about its own drops. --- */
  .releases-panel details.release { border-bottom: 1px solid var(--line); padding: 6px 0; }
  .releases-panel details.release:last-of-type { border-bottom: 0; }
  .releases-panel details.release > summary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; cursor: pointer; }
  .releases-panel details.release > summary time { font: 11px/1.4 var(--font-mono); color: var(--fg-muted); }
  .release-version { font-weight: 600; }
  .release-notes { margin: 8px 0; }
  .release-files { margin-top: 6px; }
  .release-files .col-size { width: 1%; white-space: nowrap; }
  .release-files .col-state { width: 1%; white-space: nowrap; }
  .release-files .col-dl { width: 1%; white-space: nowrap; text-align: right; }
  .release-add-form .lexxy-content, .release-add-form trix-editor { margin-bottom: 8px; }
  @media (max-width: 719px) { .release-files .col-size { display: none; } }
  @media (max-width: 479px) { .release-files .col-state { display: none; } }

  /* --- Sheets panel, picker, sheet page (unchanged shapes, tokens) --- */
  .sheets-panel .sheet-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--line); }
  .sheets-panel .sheet-row:last-child { border-bottom: 0; }
  .sheets-panel .sheet-row .muted { font: 11px/1.4 var(--font-mono); }
  /* Saved calculations (projects/_calculations, 2026-10-21): label and answer on one line, the
     expression under them in mono (it is data), the three acts after. Long expressions wrap
     rather than widen the panel. */
  /* A FULL LINE OF ITS OWN, because the rows can arrive inside a QUIET panel: "+ Add panel" opens
     it empty (`.panel--quiet` is a flex row of heading and control), and a save from the modal
     replaces only the rows (CalculationsController#create's stream), so until the next visit the
     list is a flex item there. Inert in the panel's ordinary block layout. */
  .calculations-panel .calculation-rows { flex-basis: 100%; min-width: 0; }
  .calculations-panel .calculation-list { list-style: none; margin: 0; padding: 0; }
  .calculations-panel .calculation-row { display: grid; gap: 4px; padding: 8px 0; border-bottom: 1px solid var(--line); }
  .calculations-panel .calculation-row:last-child { border-bottom: 0; }
  .calculations-panel .calculation-main { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 2px 12px; }
  .calculations-panel .calculation-label { font-weight: 500; overflow-wrap: anywhere; }
  .calculations-panel .calculation-result { font: 600 14px/1.4 var(--font-mono); overflow-wrap: anywhere; }
  .calculations-panel .calculation-expression { font: 400 12px/1.4 var(--font-mono); color: var(--fg-muted); overflow-wrap: anywhere; }
  .calculations-panel .calculation-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
  .calculations-panel .calculation-actions form { display: inline; }
  .calculations-panel .calculation-rename > summary { list-style: none; cursor: pointer; }
  .calculations-panel .calculation-rename > summary::-webkit-details-marker { display: none; }
  .calculations-panel .calculation-rename[open] { flex-basis: 100%; order: 1; }
  .calculations-panel .calculation-rename-form { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
  .calculations-panel .calculation-rename-form input[type="text"] { flex: 1; min-width: 0; }
  .calculations-panel .calculations-empty { margin: 0; font-size: 13px; }
  .sheet-new-form { display: flex; gap: 6px; align-items: center; }
  .picker-search { display: flex; gap: 8px; margin-bottom: 16px; }
  .picker-search input { flex: 1; max-width: 480px; }
  .picker-results { display: flex; flex-direction: column; gap: 6px; }
  .picker-result { display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius-panel); background: var(--surface); }
  .sheet-head { align-items: flex-start; }
  .sheet-head h1 { margin: 0 0 4px; }
  .sheet-rename { display: inline-flex; gap: 6px; }
  /* The GRID scrolls sideways, not the whole page container — the page head sits in the same
     element (it holds the Stimulus targets) and must stay put. */
  .sheet-grid { overflow-x: auto; }
  .sheet-status { font: 11px/1.4 var(--font-mono); }
  .sheet-status-error { color: var(--danger); }

  /* --- Quiet panels (stage 4, S4-1): a secondary panel with nothing in it is its heading and one
     add control on a single line, not a box saying "No entries yet." over an open composer. The
     partial sets the class when its collection is empty; a populated panel is exactly what it was.
     A composer that opens takes the whole row. The panel-head counts are hidden — a zero is not
     information. A page modifier, not a redeclaration of `.panel` (design-system rule 3). --- */
  .panel--quiet { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 8px 14px; }
  .panel--quiet > h2, .panel--quiet > .panel-head { margin: 0; flex: 1 1 auto; }
  .panel--quiet > .panel-head { display: flex; align-items: center; gap: 10px; }
  .panel--quiet > .panel-head h2 { margin: 0; }
  .panel--quiet > h2 { font-size: 13px; }
  .panel--quiet details.composer { margin: 0; }
  .panel--quiet details.composer[open] { flex-basis: 100%; }
  .panel--quiet > form { margin: 0; display: flex; gap: 6px; }
  /* `:not(.bom-rev)`: a BOM present with zero lines is quiet too, and its revision badge is a fact,
     not a count -- hiding it made "rev 3" vanish the moment the last line was removed (review). */
  .panel--quiet .panel-head .mono.muted:not(.bom-rev) { display: none; }

  /* --- The sweep's own line (stage 4, S4-2): under the Files head, above the groups. --- */
  .sweep-note { margin: 0 0 10px; font-size: 11px; }

  /* --- The README editor (stage 4, S4-4): the composer inside a README card. `.composer` itself
     is a component (components.css) and is only QUALIFIED here, never redeclared — design-system
     rule 3. The mount and the textarea are two states of one control: CodeMirror renders into the
     first and hides the second, and with no JS the second is what the maker types into. --- */
  .readme-editor { margin-top: 10px; }
  .readme-editor .readme-form { display: flex; flex-direction: column; gap: 8px; }
  .readme-editor .readme-source { width: 100%; font: 12px/1.5 var(--font-mono); resize: vertical; }
  .readme-editor .readme-mount .cm-editor { max-height: 60vh; }
  .readme-editor .readme-bar { display: flex; align-items: center; gap: 10px; }
  .readme-editor .readme-status { font-size: 12px; }
  .readme-editor .no-history-note { margin: 0 0 8px; font-size: 12px; }

  /* --- The preview banner (stage 4, S4-4): the one thing on the preview that a visitor never
     sees, so it must not look like part of the page it sits above. --- */
  .preview-banner { border: 1px solid var(--info); border-radius: var(--radius-panel);
                    background: var(--info-soft);
                    padding: 10px 14px; margin: 0 0 16px;
                    display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; }
  .preview-banner p { margin: 0; flex: 1 1 320px; font-size: 13px; }
  .preview-banner p:last-child { flex: 0 0 auto; display: flex; gap: 8px; }

  /* --- The Publish panel (stage 4, S4-4): three visibility rows that carry their own meaning,
     then the URLs. `.btn` and `.field` are components and are only used here, never redeclared
     (design-system rule 3). --- */
  .publish-panel .publish-options { border: 0; padding: 0; margin: 0 0 12px; display: flex; flex-direction: column; gap: 6px; }
  .publish-panel .publish-options legend { padding: 0; font-size: 13px; font-weight: 600; }
  .publish-option { display: grid; grid-template-columns: auto auto 1fr; align-items: baseline; gap: 4px 8px; }
  .publish-option-label { font-weight: 600; font-size: 13px; }
  .publish-option-meaning { font-size: 12px; }
  .publish-warning { font-size: 12px; }
  .publish-url, .publish-unlisted { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
                                    margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
  .publish-url .mono, .publish-unlisted .mono { font-size: 12px; overflow-wrap: anywhere; }
  .publish-unlisted p { flex-basis: 100%; margin: 0; font-size: 12px; }
  .publish-unlisted form { margin: 0; }

  /* --- The edit area (2026-10-26; spec 2026-10-26-project-edit-area §3): one head and a bar of three
     tabs over Details, Publishing and Page layout. The bar IS the page tabs' `.tab-bar` / `.tab-link`
     (components.css); only its spacing is set here, on its own class, never on the component's
     selector (design-system rule 3). The Danger zone is a `.panel` with a danger rule round it,
     because Archive is the one irreversible-looking act on these pages and should look it. --- */
  /* `.page-head.edit-head`, two classes: pages/files.css lays every `.page-head .page-title` out as a
     row, and the eyebrow sits ABOVE the name here, as it does in the project head. */
  .page-head.edit-head .page-title { display: block; min-width: 0; }
  .edit-head .eyebrow { display: block; font: 600 11px/16px var(--font-ui); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-muted); }
  .edit-head h1 { overflow-wrap: anywhere; }
  /* THE RULE IS AN INSET SHADOW, NOT THE COMPONENT'S BORDER: `.tab-link` sits 1px over the bar's
     bottom border (margin-bottom: -1px) so the current tab's underline covers it, and `.tab-bar` is a
     scroll container (overflow-x), which clips at its PADDING box — so that 1px is overflow, and a
     classic-scrollbar browser draws a vertical scrollbar beside the three links (measured: 43 vs 42).
     A shadow paints under the links instead, and nothing overflows. */
  .edit-tabs { margin: calc(-1 * var(--space-2)) 0 var(--space-5); border-bottom: 0; box-shadow: inset 0 -1px 0 var(--line); }
  .edit-tabs .tab-link { margin-bottom: 0; }
  .danger-zone { margin-top: var(--space-7); border-color: var(--danger); max-width: 640px; }
  .danger-zone-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); }
  .danger-zone-actions form { margin: 0; }
  .danger-zone-actions p { margin: 0; font-size: 12px; }
}
