  /* Fonts — self-hosted for performance */
  @font-face { font-family:'Cormorant Garamond'; font-style:normal; font-weight:400; font-display:swap; src:url('fonts/cormorant-garamond-v21-latin-regular.woff2') format('woff2'); }
  @font-face { font-family:'Cormorant Garamond'; font-style:normal; font-weight:500; font-display:swap; src:url('fonts/cormorant-garamond-v21-latin-500.woff2') format('woff2'); }
  @font-face { font-family:'Cormorant Garamond'; font-style:italic; font-weight:400; font-display:swap; src:url('fonts/cormorant-garamond-v21-latin-italic.woff2') format('woff2'); }
  @font-face { font-family:'Montserrat'; font-style:normal; font-weight:100 900; font-display:swap; src:url('fonts/montserrat-variable.woff2') format('woff2'); }
  @font-face { font-family:'Montserrat'; font-style:italic; font-weight:100 900; font-display:swap; src:url('fonts/montserrat-italic-variable.woff2') format('woff2'); }

  html { font-size: 15px; }

  :root {
    --bg: #f4f0ec; --bg-warm: #eae5df;
    --text: #2a2623; --text-soft: #655d55; --text-faint: #756b62;
    --accent: #7a6150; --accent-hover: #725a49; --accent-light: #7d6b59;
    --border: #d6cdc4; --border-light: #e4ddd6;
    --panel: #faf8f5; --card: #f0ebe6; --canvas-bg: #fff;
    --shadow: rgba(42,38,35,0.07);
    --radius: 5px; --radius-lg: 8px;

    /* Type tokens — single source of truth for typography. Seven levels
       from largest (--font-1, ~26px) to smallest (--font-7, ~9px), built
       as a 1.2 modular scale anchored at --font-4 = 1rem = 15px. Change
       html { font-size } above to shift every level proportionally; edit
       individual rem values here to adjust single steps.

       The smallest level (--font-7, ~9px) is reserved for dense data
       displays — swatch indices, thread cells, badges — where the visible
       difference between 9px and 11px materially affects layout.

       Display sizes larger than --font-1 (32px page titles, 48px display
       headlines) are currently inline literals; promote them to
       --font-display-1, --font-display-2 if/when needed. */
    --font-1: 1.728rem;  /* 25.9px */
    --font-2: 1.44rem;   /* 21.6px */
    --font-3: 1.2rem;    /* 18px   */
    --font-4: 1rem;      /* 15px — body baseline */
    --font-5: 0.833rem;  /* 12.5px */
    --font-6: 0.694rem;  /* 10.4px */
    --font-7: 0.579rem;  /* 8.7px  — dense data only */

    /* Font families. Montserrat (Julieta Ulanovsky, 2011) is the UI default;
       Cormorant Garamond is the display serif (masthead, page titles, the
       Studio entrance heading); monospace is used for code-adjacent fields
       like sequence inputs. */
    --font-sans:  'Montserrat', sans-serif;
    --font-serif: 'Cormorant Garamond', serif;
    --font-mono:  ui-monospace, Menlo, Consolas, monospace;

    /* Weights — Montserrat carries Thin (100) through Black (900); loaded
       as two variable fonts (normal + italic), so any integer weight in
       100–900 is available without further @font-face declarations. Define
       new --weight-* tokens here as the design system grows. */
    --weight-light:   300;
    --weight-regular: 400;
    --weight-medium:  500;
  }

  * { margin:0; padding:0; box-sizing:border-box; }
  body { font-family:var(--font-sans); font-weight:var(--weight-light); background:var(--bg); color:var(--text); min-height:100vh; font-size:var(--font-2); }

  /* Layout
     Rows:
       1. header        — full width
       2. tab band      — full width; tab strip lives at left, right side is blank
       3. content       — three columns: cloth | resize handle | right panel
       4. favorites bar — full width
     This puts the right panel on the same vertical baseline as the cloth content
     (below the tab strip), so the header reads as a continuous band across the
     window and the panel doesn't have a dead space above it. */
  /* Auth bar — slim full-width strip at the very top of the editor.
     Defined as a custom property so .app's viewport math can subtract it
     without hardcoding the height in two places. The fallback (0px) means
     pages without an auth bar (index.php, admin.php with site-bar) keep
     their existing 100vh layout untouched. */
  :root { --auth-bar-height: 52px; }
  .auth-bar {
    height: var(--auth-bar-height);
    background: var(--bg-warm);
    display: flex;
    align-items: center;
    padding: 0 1.6rem;
    gap: 1.2rem;
    flex-shrink: 0;
    box-sizing: border-box;
  }
  .auth-bar-lockup {
    display: flex; align-items: center; gap: 0.533rem;
    text-decoration: none;
    color: var(--text);
    transition: color 0.15s;
  }
  .auth-bar-lockup:hover { color: var(--accent); }
  .auth-bar-wordmark {
    font-family: var(--font-serif);
    font-size: 23px;
    font-weight: var(--weight-regular);
    letter-spacing: 0;
    line-height: 1;
  }
  .auth-bar-beta {
    display: inline-block;
    background: var(--accent);
    color: #fff;
    font-family: var(--font-sans);
    font-size: var(--font-7);
    font-weight: var(--weight-medium);
    letter-spacing: 0.5px;
    text-transform: lowercase;
    padding: 2px 0.533rem;
    border-radius: 999px;
    line-height: 1.4;
    margin-left: 2px;
    align-self: center;
    position: relative;
    top: 2px;
  }
  .auth-bar-nav {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-family: var(--font-sans);
  }
  .auth-bar-nav-link {
    font-size: var(--font-4);
    color: var(--text-soft);
    text-decoration: none;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    transition: color 0.12s, background 0.12s;
  }
  a.auth-bar-nav-link:hover { color: var(--accent); background: var(--bg-warm); }
  .auth-bar-nav-link.is-current { color: var(--text); font-weight: var(--weight-medium); }
  .auth-bar-nav-sep { color: #4a423b; font-size: var(--font-4); user-select: none; }

  /* "Send to Showroom" control in the editor's cloth footer bar. */
  .showroom-btn {
    font-family: var(--font-sans);
    font-size: var(--font-4);
    font-weight: var(--weight-medium);
    background: var(--accent); color: #fff;
    border: 1px solid var(--accent); border-radius: 6px;
    padding: 0.4rem 1rem; cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .showroom-btn:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
  .showroom-btn:disabled { opacity: 0.6; cursor: default; }
  .showroom-live:not([hidden]) { display: flex; align-items: center; gap: 14px; font-family: var(--font-sans); font-size: var(--font-4); }
  .showroom-live-label { color: var(--text-soft); }
  .showroom-link, .showroom-link-btn {
    color: var(--accent); text-decoration: none;
    background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
  }
  .showroom-link:hover, .showroom-link-btn:hover { text-decoration: underline; }
  .auth-bar-inner {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.933rem;
    font-family: var(--font-sans);
    font-size: var(--font-4);
  }
  .auth-bar-user { color: var(--text-soft); }
  .auth-bar-user-link {
    color: var(--text-soft);
    text-decoration: none;
    border-bottom: 1px dotted transparent;
    transition: color 0.12s, border-color 0.12s;
  }
  .auth-bar-user-link:hover {
    color: var(--accent);
    border-bottom-color: var(--accent);
  }
  .auth-bar-link {
    color: var(--text-soft);
    text-decoration: none;
    padding: 0.25rem 0.667rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    transition: all 0.15s;
  }
  .auth-bar-link:hover { color: var(--accent); border-color: var(--accent); }

  /* Signed-in user flyout. The trigger reads like the old name link (plus a
     caret); clicking drops a small card with Preferences / Sign out. The
     [hidden] rule is load-bearing: without it the display:flex below would
     override the hidden attribute's default display:none and the menu would
     never hide. */
  .auth-bar-user-menu { position: relative; display: inline-flex; }
  .auth-bar-user-trigger {
    font-family: var(--font-sans);
    font-size: var(--font-4);
    font-weight: var(--weight-medium);
    color: var(--text-soft);
    background: none;
    border: none;
    cursor: pointer;
    padding: 0.25rem 0.4rem;
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    transition: color 0.12s;
  }
  .auth-bar-user-trigger:hover,
  .auth-bar-user-trigger[aria-expanded="true"] { color: var(--accent); }
  .auth-bar-user-caret { font-size: 0.7em; opacity: 0.7; }
  .auth-bar-user-flyout {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    min-width: 9rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.10);
    padding: 0.3rem;
    display: flex;
    flex-direction: column;
    gap: 1px;
    z-index: 100;
  }
  .auth-bar-user-flyout[hidden] { display: none; }
  .auth-bar-flyout-item {
    color: var(--text-soft);
    text-decoration: none;
    padding: 0.4rem 0.6rem;
    border-radius: 4px;
    font-size: var(--font-4);
    white-space: nowrap;
    transition: background 0.12s, color 0.12s;
  }
  .auth-bar-flyout-item:hover { background: var(--bg-warm); color: var(--accent); }

  .app { display:grid; grid-template-columns:240px 1fr 5px 300px; grid-template-rows:auto auto 1fr auto; height:calc(100vh - var(--auth-bar-height, 0px) - var(--guest-banner-height, 0px)); overflow:hidden; min-width:0; box-sizing:border-box; position:relative; transition:grid-template-columns 0.18s ease; }
  .app.panels-hidden { grid-template-columns:1fr; }
  .app.panels-hidden .right-panel, .app.panels-hidden .resize-handle, .app.panels-hidden .drafts-rail { display:none; }
  /* Independent collapse of just the drafts panel ("My Library"). The first
     column narrows to a slim action strip (see ".app.library-hidden" rules in
     the drafts-panel section below): the panel stays in the DOM, but only the
     Save / Save-as-new buttons remain visible, with everything else hidden. */
  .app.panels-hidden .center-col { grid-column:1; }
  .app.panels-hidden .center-tabs-inner { grid-column:1; }

  /* Center column */
  .center-col { display:flex; flex-direction:column; min-height:0; min-width:0; grid-column:2; grid-row:3; }
  .center-scroll { flex:1; min-height:0; display:flex; flex-direction:column; }
  /* Tab strip spans the full width of the app grid so the bottom-border line
     runs unbroken from edge to edge. Inside, a 3-column grid mirrors the app
     layout so the tabs themselves center over the cloth pane (column 1) and
     the right portion of the strip stays empty over the resize handle and
     right panel. Flex-centering the tab buttons within the first cell does
     the work; the whole thing tracks sidebar resize automatically since the
     parent grid template is the source of truth. */
  .center-tabs { grid-column:1/-1; display:grid; grid-template-columns:subgrid; background:var(--bg-warm); border-bottom:1px solid var(--border); z-index:10; position:relative; }
  .center-tabs-inner { grid-column:2; display:flex; gap:7px; padding:calc(0.4rem + 10px) 1.067rem 0; justify-content:center; align-items:flex-end; }
  .center-tab { font-family:var(--font-sans); font-size:var(--font-4); font-weight:var(--weight-regular); padding:0.467rem 1.2rem 0.533rem; border:1px solid var(--border); border-top-left-radius:6px; border-top-right-radius:6px; background:var(--card); color:var(--text-faint); cursor:pointer; transition:all 0.12s; margin-bottom:-1px; }
  .center-tab:hover { color:var(--text-soft); background:var(--panel); border-color:var(--border); }
  .center-tab.active { color:var(--text); font-weight:var(--weight-medium); background:var(--bg); border-color:var(--border); border-bottom-color:var(--bg); }
  .center-tab-divider { display:inline-block; width:1px; align-self:stretch; margin:0.533rem 0.667rem 0.25rem; background:var(--border); }
  /* Panel-toggle pinned to the right edge of the tabs row, aligned to
     the same baseline as the tab buttons. Sits in the .center-tabs
     container but isn't part of the centered .center-tabs-inner flex. */
  .center-tabs .panel-toggle-btn { position:absolute; right:16px; bottom:6px; z-index:11; }

  /* Setup tab is three independent columns of graduated width — column 1
     narrowest (Name, Materials, Reed, Floating Selvedges, Profiles), column 2
     a little wider (Sett, Warp Plan, Loom, Threading direction), column 3
     widest for the Notes field. Each column sizes to its own content (no
     shared rows), so the form sections stay visible without the Notes field
     pushing them down. The form mounts into #drawerBody (col 1) and
     #drawerColRight (col 2) via renderScratchPad; the Notes field is static
     markup so its Edit/Save flow and Quill instance survive every rebuild. */
  .setup-layout { max-width:880px; margin:0 auto; display:grid; grid-template-columns:minmax(0,1fr) max-content max-content minmax(0,1fr); column-gap:3.5rem; row-gap:1.2rem; align-items:start; }
  .setup-full-row { grid-column:1 / -1; }
  .setup-col { display:flex; flex-direction:column; gap:1.333rem; min-width:0; }
  /* The two columns are content-sized and ride between flexible spacers, so they
     sit together near the centre instead of being flung to the page edges. */
  #drawerBody { grid-column:2; }
  #drawerColRight { grid-column:3; }
  @media (max-width: 880px){
    .setup-layout { grid-template-columns:1fr; }
    #drawerBody, #drawerColRight { grid-column:auto; }
  }

  /* Notes tab — a single-column writing surface designed like a journal
     page. The wrapper carries the visual identity: a warm tinted ground
     (--card sits one shade darker than --bg) with a quiet loom watermark
     stamped in the lower-right corner. The textarea inside is transparent
     and borderless, sitting on top of the wrapper's surface so the watermark
     reads BENEATH your typing rather than on top of it.

     The watermark uses mask-image, which means the SVG just defines the
     *shape* — color and opacity are controlled here in CSS, so any loom
     silhouette will read as a unified tonal wash. If your SVG ends up
     showing as a blank rectangle instead of a shape, it's likely
     stroke-only with no filled paths — convert strokes to fills (most
     SVG editors have this) and it'll mask correctly. */
  .notes-wrap-inner { position:relative; max-width:760px; margin:0 auto; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; }
  /* Thread-spool watermark — a small mark tucked into the bottom-right corner
     of the Materials card (.material-slot, set in renderMaterialSlot). */
  .material-slot { position:relative; }
  .material-slot::before {
    content:'';
    position:absolute;
    bottom:8px;
    right:8px;
    width:32px;
    height:32px;
    background-color:var(--text-soft);
    -webkit-mask-image:var(--loom-watermark, url('./loom-watermark.svg'));
            mask-image:var(--loom-watermark, url('./loom-watermark.svg'));
    -webkit-mask-size:contain;
            mask-size:contain;
    -webkit-mask-repeat:no-repeat;
            mask-repeat:no-repeat;
    -webkit-mask-position:bottom right;
            mask-position:bottom right;
    opacity:0.15;
    pointer-events:none;
    z-index:0;
  }
  .notes-wrap-inner .drawer-notes { width:100%; min-height:25vh; resize:vertical; background:transparent; border:none; position:relative; z-index:1; }
  /* Quill (rich-text Notes) fitted to the Weavery palette and sat transparently
     on the .notes-wrap-inner card so the card background and watermark show
     through. z-index keeps the toolbar/editor above the card's ::before. */
  .notes-wrap-inner .ql-toolbar.ql-snow { border:none; border-bottom:1px solid var(--border); padding:8px 10px; position:relative; z-index:1; }
  .notes-wrap-inner .ql-container.ql-snow { border:none; font-family:var(--font-sans); position:relative; z-index:1; }
  .notes-wrap-inner .ql-editor { min-height:25vh; font-size:var(--font-4); font-weight:var(--weight-light); color:var(--text); line-height:1.6; }
  .notes-wrap-inner .ql-editor.ql-blank::before { color:var(--text-faint); font-style:normal; left:0.667rem; right:0.667rem; }
  .notes-wrap-inner .ql-snow .ql-stroke { stroke:var(--text-soft); }
  .notes-wrap-inner .ql-snow .ql-fill { fill:var(--text-soft); }
  .notes-wrap-inner .ql-snow .ql-picker { color:var(--text-soft); }
  .notes-wrap-inner .ql-snow.ql-toolbar button:hover .ql-stroke, .notes-wrap-inner .ql-snow.ql-toolbar button.ql-active .ql-stroke, .notes-wrap-inner .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke { stroke:var(--accent); }
  .notes-wrap-inner .ql-snow.ql-toolbar button:hover .ql-fill, .notes-wrap-inner .ql-snow.ql-toolbar button.ql-active .ql-fill { fill:var(--accent); }
  /* Notes read-only display (the Edit/Save flow's default view): the rendered
     note sits on the same card as the editor, at full natural height so the
     whole note is visible at once. .notes-display-body lifts above the card's
     watermark ::before (z-index:1), matching the Quill containers above. */
  .notes-display-body { position:relative; z-index:1; min-height:300px; padding:0.667rem 0.933rem; font-family:var(--font-sans); font-size:var(--font-4); font-weight:var(--weight-light); color:var(--text); line-height:1.6; overflow-wrap:break-word; }
  .notes-display-body.notes-empty { color:var(--text-faint); font-style:italic; }
  .notes-display-body > :first-child { margin-top:0; }
  .notes-display-body > :last-child { margin-bottom:0; }
  .notes-display-body p { margin:0 0 0.6em; }
  .notes-display-body h1, .notes-display-body h2, .notes-display-body h3 { margin:0.7em 0 0.3em; line-height:1.2; }
  .notes-display-body h1 { font-size:var(--font-2); }
  .notes-display-body h2 { font-size:var(--font-3); }
  .notes-display-body h3 { font-size:var(--font-4); }
  .notes-display-body ul, .notes-display-body ol { margin:0 0 0.6em; padding-left:1.4em; }
  .notes-display-body blockquote { margin:0 0 0.6em; padding-left:0.8em; border-left:2px solid var(--border); color:var(--text-soft); }
  .notes-actions { display:flex; gap:8px; margin-top:8px; }

  /* Empty-draft placeholder shown in the centre of the cloth area when there
     are no cards yet (renderCanvas toggles #emptyState). margin:auto centres
     it both ways inside .main's flex column; the collapsed (~0) #mainCloth
     above it is invisible. The mark is a masked watermark in the same style
     as the notes watermark (text-soft fill, faint), just larger and centred. */
  .empty-state { margin:auto; transform:translateY(-8vh); display:flex; flex-direction:column; align-items:center; gap:1.2rem; text-align:center; pointer-events:none; user-select:none; max-width:320px; }
  .empty-state-mark {
    width:104px;
    height:104px;
    background-color:var(--text-soft);
    -webkit-mask-image:var(--empty-watermark, url('./empty.svg'));
            mask-image:var(--empty-watermark, url('./empty.svg'));
    -webkit-mask-size:contain;
            mask-size:contain;
    -webkit-mask-repeat:no-repeat;
            mask-repeat:no-repeat;
    -webkit-mask-position:center;
            mask-position:center;
    opacity:0.12;
  }
  .empty-state-text { color:var(--text-soft); font-size:var(--font-3); line-height:1.5; }

  /* Dye Lab tab: two-column upper region for inputs and outputs. Left
     column carries color selection (the visual side); right column
     carries weights, recipe, and the print action (the numeric side).
     The process steps and disclaimer sit below at full width. */
  .dyelab-grid { display:grid; grid-template-columns:1fr 1.4fr; gap:20px; align-items:start; }
  .dyelab-col { display:flex; flex-direction:column; gap:20px; }
  /* Dye-type switch: underline tabs in the manner of the library's .lib-tab.
     First tab is flush-left so its label lines up with the process title below. */
  .dye-type-tabs { display:flex; gap:2px; border-bottom:1px solid var(--border); margin:0 0.8rem; }
  .dye-type-tab {
    background:none; border:none; cursor:pointer; font:inherit;
    font-size:var(--font-4); color:var(--text-soft);
    padding:10px 14px; margin-bottom:-1px;
    border-bottom:2px solid transparent;
    transition:color 0.15s ease, border-color 0.15s ease;
  }
  .dye-type-tab:first-child { padding-left:0; }
  .dye-type-tab:hover { color:var(--text); }
  .dye-type-tab.is-active { color:var(--text); border-bottom-color:var(--accent); font-weight:500; }
  @media (max-width: 880px){
    .dyelab-grid { grid-template-columns:1fr; }
  }


  /* Header */
  .header { grid-column:1/-1; padding:0.533rem 1.6rem; display:flex; align-items:center; gap:0.933rem; background:var(--bg); flex-wrap:wrap; min-height:0; }
  .header h1 { font-family:var(--font-serif); font-size:26px; font-weight:var(--weight-regular); }
  .header-right { margin-left:auto; display:flex; align-items:center; gap:0.667rem; flex-wrap:wrap; }
  .zoom-controls { display:flex; gap:0.4rem; align-items:center; }
  .zoom-label { font-size:var(--font-4); color:var(--text-soft); min-width:28px; text-align:center; }

  /* Right panel */
  .right-panel { background:var(--panel); border-left:1px solid var(--border); overflow-y:auto; display:flex; flex-direction:column; padding-bottom:1.333rem; grid-column:4; grid-row:3; }
  .resize-handle { width:5px; cursor:col-resize; background:transparent; position:relative; z-index:10; flex-shrink:0; grid-column:3; grid-row:3; }

  /* Drafts panel — left column. Holds Save/Save-as-new/New-from-preset/Import,
     plus the active draft and the rest of the user's drafts. Replaces the
     bottom favorites strip. */
  /* The drafts rail is the grid cell for column 1. It owns the panel's
     background and right-border so those stay visible even when the rail
     is shrunk to door-knob width (28px). The drafts-panel sits inside as
     content; the library-toggle sits inside as a sibling, absolutely
     positioned at top-right so it tracks the rail's right edge as the
     rail width animates. */
  .drafts-rail { grid-column:1; grid-row:3; position:relative; overflow:hidden; background:var(--bg-warm); border-right:1px solid var(--border); min-width:0; display:flex; flex-direction:column; }

  .drafts-panel { background:var(--panel); overflow-x:hidden; overflow-y:auto; overflow-anchor:none; min-width:0; width:240px; flex:1 1 auto; min-height:0; box-sizing:border-box; display:block; padding:0 0.8rem 1.333rem; transition:opacity 0.18s ease; }
  /* Block scroll container (not flex): scrolls to the bottom cleanly and
     honors padding-bottom. The inter-child spacing the old flex `gap` gave is
     restored with a top margin on every child after the first. */
  .drafts-panel > * + * { margin-top:0.8rem; }
  /* Sticky top button block: Save / Save-as-new / New-from-preset / Import WIF
     / library utilities. Stays pinned to the top of .drafts-panel while the
     drafts list scrolls beneath it. .drafts-panel has no top padding (the
     head's own padding-top provides the inset) so the head sits flush at
     top:0 with no band for cards to peek through; the negative side margins
     bleed its --panel background edge-to-edge so cards tuck cleanly under.
     z-index keeps it above the scrolling cards. */
  .drafts-panel-sticky { position:sticky; top:0; z-index:2; background:var(--panel); display:flex; flex-direction:column; gap:0.4rem; margin-left:-0.8rem; margin-right:-0.8rem; padding:0.667rem 0.8rem 0.6rem; }
  .library-header {
    flex-shrink:0;
    padding:0.667rem 1.2rem;
    margin:0;
    display:flex; align-items:center; gap:0.5rem;
    font-size:var(--font-6); font-weight:var(--weight-medium); letter-spacing:0.12em; text-transform:uppercase;
    color:var(--text-faint); user-select:none;
    border-bottom:1px solid var(--border-light);
    background:var(--panel);
  }
  .library-header-title { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  /* Doorknob toggle: inline flex item inside .library-header, sits to the
     right of the "My Library" title. The header stays put across open and
     closed states, so the doorknob stays put too — clicking it toggles
     visibility of the panel content below. */
  .library-toggle {
    flex-shrink:0;
    width:28px; height:22px;
    background:#a89e94;
    border:1px solid #a89e94;
    border-radius:4px;
    cursor:pointer;
    color:var(--canvas-bg);
    padding:0;
    display:flex; align-items:center; justify-content:center;
    font-size:13px; line-height:1;
    transition:color 0.12s, border-color 0.12s, background 0.12s;
    user-select:none;
  }
  /* Toggle shows a sidebar-panel glyph (inline SVG in the button), not an
     arrow — the old ‹/› was easily confused with the "← Studio entrance"
     link. The same icon shows open and closed; the title attribute and the
     rail's own motion convey what it does. */
  .library-toggle svg { width:15px; height:15px; display:block; stroke:currentColor; fill:none; }
  .library-toggle:hover { color:var(--canvas-bg); border-color:var(--accent-light); background:var(--accent-light); }
  .drafts-panel-actions { display:flex; flex-direction:column; gap:0.4rem; }
  /* Revert pill — the single revert control, in the Save block. Hidden until
     the draft is dirty, then shown in every view. Toggled by _refreshSaveButton. */
  .drafts-actions-revert { display:none; }
  .drafts-actions-revert.dirty { display:block; }
  .drafts-actions-revert.armed { background:var(--accent); color:var(--canvas-bg); border-color:var(--accent); }
  /* Save / Save as new pills are sized at font-5 to read at the same scale as
     the "New from preset" select below them — they're peer-level moves in the
     library top-bar, so they should look like peers. */
  .drafts-panel-actions .pill { width:100%; text-align:center; box-sizing:border-box; font-size:var(--font-5); padding:5px 0.667rem; }
  .drafts-panel-actions .pill-primary { background:var(--accent); color:var(--canvas-bg); border-color:var(--accent); }
  .drafts-panel-actions .pill-primary:hover { background:var(--accent-hover); border-color:var(--accent-hover); }
  /* Honest Save: the last save didn't reach the server. Distinct and clickable,
     overriding both the accent fill and the disabled fade (save-error is never
     disabled, so the user can always retry). */
  .drafts-panel-actions .pill-primary.save-error,
  .drafts-panel-actions .pill-primary.save-error:hover {
    background:#a8362a; border-color:#a8362a; color:#fff; opacity:1; cursor:pointer;
  }
  .drafts-panel-actions .pill-primary:disabled {
    opacity: 1;
    cursor: not-allowed;
    background: var(--bg-warm);
    color: var(--text-faint);
    border-color: var(--border);
  }

  /* ── Collapsed library ("My Library" toggled closed) ──────────────────────
     The rail narrows to a slim strip (64px — matched in _applyAppGrid and the
     resize-drag handler in weavery.js). The panel stays live but shows only
     the Save / Save-as-new buttons; the preset dropdown, import/export group,
     and the drafts list are all hidden. The header keeps just the doorknob
     (the "My Library" title is dropped — it can't fit the narrow rail). The
     button labels wrap rather than abbreviate, so "Save as new" reads as two
     short lines. */
  .app.library-hidden .library-header { padding:0.667rem 0; justify-content:center; }
  .app.library-hidden .library-header-title { display:none; }
  .app.library-hidden .drafts-panel { width:100%; padding:0.6rem 0.3rem 1rem; gap:0.5rem; }
  .app.library-hidden .drafts-panel > * { display:none; }
  /* The action buttons now live inside .drafts-panel-sticky, so reveal that
     wrapper (not .drafts-panel-actions directly), strip its sticky/bleed
     styling for the narrow rail, and hide everything in it except the
     Save / Save-as-new buttons. */
  .app.library-hidden .drafts-panel > .drafts-panel-sticky { display:flex; }
  .app.library-hidden .drafts-panel-sticky { position:static; margin:0; padding:0; background:none; }
  .app.library-hidden .drafts-panel-sticky > *:not(.drafts-panel-actions) { display:none; }
  .app.library-hidden .drafts-panel-actions .pill { font-size:var(--font-6); padding:6px 4px; white-space:normal; line-height:1.2; }
  .drafts-panel-divider { border-top:1px solid var(--border); margin:0; }
  /* Centering wrapper for the Collection bundle actions. Three .pill buttons
     (Export / Import / Save to Library) flow inline and wrap if the panel is
     narrow — sits below the "Collection" header, inside the sticky head. */
  .drafts-panel-utilities { display:flex; flex-direction:row; flex-wrap:wrap; justify-content:center; gap:0.333rem 0.4rem; }
  .drafts-panel-empty { font-size:var(--font-5); color:var(--text-faint); padding:0.533rem 0.25rem; line-height:1.4; }
  .drafts-panel-section-label { font-size:var(--font-6); color:var(--text-faint); text-transform:uppercase; letter-spacing:0.5px; padding:0 0.25rem; display:flex; align-items:center; gap:0.4rem; }
  .drafts-panel-dirty-dot { width:6px; height:6px; border-radius:50%; background:var(--accent); display:inline-block; }
  .drafts-card { background:var(--bg); border:1px solid var(--border); border-radius:4px; padding:0.4rem; cursor:pointer; transition:all 0.12s; position:relative; }
  .drafts-card:hover { border-color:var(--accent); }
  .drafts-card.active { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); cursor:default; }
  .drafts-card-thumb { width:100%; height:auto; aspect-ratio:4/3; background:var(--canvas-bg); border-radius:2px; display:block; object-fit:contain; image-rendering:pixelated; image-rendering:crisp-edges; }
  /* Placeholder while a card's thumb is waiting to be lazily generated.
     Holds the same box dimensions as the real thumb (via aspect-ratio
     above) so no layout shift occurs when the <img> swaps in. The
     centered favicon pattern reads as a "Weavery loading" state — the
     plainest possible weave standing in for a yet-to-render draft. */
  .drafts-card-thumb-placeholder {
    background-color:var(--canvas-bg);
    background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjN2Q2YjU5IiBkPSJNMjQ0LjMzNywzOC4yMjlMNDQuMjk4LDIxNC4yNjNjLTUuNzE0LDUuMDI3LTguOTg4LDEyLjI3LTguOTg4LDE5Ljg4MXYyMzUuMkg0NzYuNjl2LTIzNS4yYzAtNy42MS0zLjI3NC0xNC44NTQtOC45ODgtMTkuODgxTDI2Ny42NjMsMzguMjI5QzI2MC45OTUsMzIuMzYxLDI1MS4wMDUsMzIuMzYxLDI0NC4zMzcsMzguMjI5eiIvPjxwb2x5Z29uIGZpbGw9IiNlYWU1ZGYiIHBvaW50cz0iMjU2LDcyLjEwMyA3MC42MjEsMjM1LjIzNyA3MC42MjEsNDY5LjM0NCA0NDEuMzc5LDQ2OS4zNDQgNDQxLjM3OSwyMzUuMjM3Ii8+PHBvbHlnb24gZmlsbD0iI2Y0ZjBlYyIgcG9pbnRzPSIyNTYsMTE2LjI0MSAxMDUuOTMxLDI0OC4zMDIgMTA1LjkzMSw0NjkuMzQ0IDQwNi4wNjksNDY5LjM0NCA0MDYuMDY5LDI0OC4zMDIiLz48cG9seWdvbiBmaWxsPSIjZmFmOGY1IiBwb2ludHM9IjI1NiwxNjAuMzc5IDE0MS4yNDEsMjYxLjM2NiAxNDEuMjQxLDQ2OS4zNDQgMzcwLjc1OSw0NjkuMzQ0IDM3MC43NTksMjYxLjM2NiIvPjxwYXRoIGZpbGw9IiM3YTYxNTAiIGQ9Ik0xMjMuNjg5LDI2My44NmMtMC43MjktMy43NDcsMi41MjgtNy4wMDQsNi4yNzQtNi4yNzRjMTcuMTg0LDMuMzQzLDU5LjUzLDEzLjc1Nyw4NC40NjIsMzguNjg5YzE3LjU3OSwxNy41NzksMjAuMTc5LDQzLjQ4MSw1LjgwNiw1Ny44NTRjLTE0LjM3MiwxNC4zNzItNDAuMjc1LDExLjc3My01Ny44NTQtNS44MDZDMTM3LjQ0NywzMjMuMzkxLDEyNy4wMzIsMjgxLjA0NCwxMjMuNjg5LDI2My44NnoiLz48cGF0aCBmaWxsPSIjNzI1YTQ5IiBkPSJNMTYyLjM3OCwzNDguMzIyYzE3LjU3OSwxNy41NzksNDMuNDgxLDIwLjE3OSw1Ny44NTQsNS44MDZsLTk1LjA4NC05NS4wODRjLTEuMTkyLDEuMTkyLTEuODIzLDIuOTQyLTEuNDU5LDQuODE1QzEyNy4wMzIsMjgxLjA0NCwxMzcuNDQ3LDMyMy4zOSwxNjIuMzc4LDM0OC4zMjJ6Ii8+PHBhdGggZmlsbD0iIzdhNjE1MCIgZD0iTTM2OS42ODEsMjM3LjI0OGMwLjYtMy4wODctMi4wODItNS43NzEtNS4xNy01LjE3Yy0xNC4xNTksMi43NTQtNDkuMDUsMTEuMzM2LTY5LjU5MiwzMS44NzhjLTE0LjQ4NSwxNC40ODUtMTYuNjI3LDM1LjgyNy00Ljc4Myw0Ny42NjljMTEuODQyLDExLjg0MiwzMy4xODQsOS43LDQ3LjY2OS00Ljc4M0MzNTguMzQ1LDI4Ni4yOTksMzY2LjkyNiwyNTEuNDA4LDM2OS42ODEsMjM3LjI0OHoiLz48cGF0aCBmaWxsPSIjNzI1YTQ5IiBkPSJNMzY0LjUxLDIzMi4wNzljLTE0LjE1OSwyLjc1NC00OS4wNSwxMS4zMzUtNjkuNTkyLDMxLjg3OGMtMTQuNDg0LDE0LjQ4NC0xNi42MjYsMzUuODI3LTQuNzgzLDQ3LjY2OWw3OC4zNDUtNzguMzQ1QzM2Ny40OTYsMjMyLjI5OCwzNjYuMDUzLDIzMS43NzcsMzY0LjUxLDIzMi4wNzl6Ii8+PHBhdGggZmlsbD0iIzY1NWQ1NSIgZD0iTTMzMi44NjIsMjY4Ljg5NmMtMy40NDgtMy40NDgtOS4wMzUtMy40NDgtMTIuNDgzLDBMMjY1LjI3NSwzMjRjLTExLjY3MSwxMS42NzItMTguMTAyLDI3LjE4OS0xOC4xMDIsNDMuNjk4djAuODg4bC02NC4zOC02NC4zOGMtMy40NDgtMy40NDgtOS4wMzUtMy40NDgtMTIuNDgzLDBjLTMuNDQ4LDMuNDQ4LTMuNDQ4LDkuMDM1LDAsMTIuNDgzbDc2Ljg2Myw3Ni44NjJ2NzUuNzk0aDE3LjY1NVYzNjcuNjk5YzAtMTEuNzk0LDQuNTk1LTIyLjg3OSwxMi45MzEtMzEuMjE1bDU1LjEwMy01NS4xMDRDMzM2LjMxLDI3Ny45MzEsMzM2LjMxLDI3Mi4zNDUsMzMyLjg2MiwyNjguODk2eiIvPjxwYXRoIGZpbGw9IiNkNmNkYzQiIGQ9Ik01MDMuMTcyLDQ3OC4xNzJIOC44MjhjLTQuODc5LDAtOC44MjgtMy45NDgtOC44MjgtOC44MjhzMy45NDgtOC44MjgsOC44MjgtOC44MjhoNDk0LjM0NWM0Ljg3OSwwLDguODI4LDMuOTQ4LDguODI4LDguODI4UzUwOC4wNTIsNDc4LjE3Miw1MDMuMTcyLDQ3OC4xNzJ6Ii8+PC9zdmc+Cg==");
    background-repeat:no-repeat;
    background-position:center;
    background-size:48px 48px;
    opacity:0.55;
  }
  .drafts-card-name { font-size:var(--font-5); color:var(--text-soft); margin-top:0.25rem; padding:0 2px; word-break:break-word; }
  .drafts-card-name-row { display:flex; align-items:flex-start; gap:4px; margin-top:0.25rem; }
  .drafts-card-name-row .drafts-card-name { margin-top:0; flex:1 1 auto; min-width:0; }
  /* Pencil affordance on the draft name — always visible but quiet, so it reads
     as "editable" without competing with the name. Opens the rename modal. */
  .drafts-card-rename { flex:0 0 auto; margin-top:1px; padding:1px; background:none; border:none; color:var(--text-faint); cursor:pointer; opacity:0.65; transition:opacity 0.12s, color 0.12s; display:flex; align-items:center; justify-content:center; }
  .drafts-card-rename:hover { opacity:1; color:var(--accent); }
  .drafts-card-setup { font-size:var(--font-6); color:var(--text-faint); margin-top:2px; padding:0 2px; word-break:break-word; }

  /* Setup drawer — Profiles section. Each profile is a pill: a click-to-
     apply label plus a hover-reveal × to delete. The "+ Save current" pill
     at the end of the list bootstraps a new profile from S.scratch. */
  .profiles-list { display:flex; flex-wrap:wrap; gap:0.4rem; padding:0.25rem 0; }
  .profiles-empty { font-size:var(--font-5); color:var(--text-faint); padding:0.25rem 0 0.533rem; line-height:1.4; }
  .profile-pill { display:inline-flex; align-items:center; border:1px solid var(--border); border-radius:14px; background:var(--canvas-bg); transition:border-color 0.12s; overflow:hidden; }
  .profile-pill:hover { border-color:var(--accent); }
  .profile-pill-label { font-family:var(--font-sans); font-size:var(--font-5); color:var(--text-soft); padding:0.333rem 0.25rem 0.333rem 0.8rem; background:none; border:none; cursor:pointer; }
  .profile-pill:hover .profile-pill-label { color:var(--accent); }
  .profile-pill-remove { background:none; border:none; color:var(--text-faint); padding:0.25rem 0.533rem 0.25rem 2px; cursor:pointer; font-size:14px; line-height:1; opacity:0; transition:opacity 0.12s; }
  .profile-pill:hover .profile-pill-remove { opacity:1; }
  .profile-pill-remove:hover { color:var(--accent); }
  .profile-pill-add, .profile-pill-reset { display:inline-flex; align-items:center; font-family:var(--font-sans); font-size:var(--font-5); color:var(--text-faint); padding:0.333rem 0.8rem; background:transparent; border:1px dashed var(--border); border-radius:14px; cursor:pointer; transition:all 0.12s; }
  .profile-pill-add:hover, .profile-pill-reset:hover { color:var(--accent); border-color:var(--accent); }
  .drafts-card-remove { position:absolute; top:-6px; right:-6px; width:20px; height:20px; border-radius:50%; background:var(--text-soft); color:var(--canvas-bg); border:none; font-size:14px; line-height:1; cursor:pointer; opacity:0; transition:opacity 0.12s; padding:0; display:flex; align-items:center; justify-content:center; }
  .drafts-card:hover .drafts-card-remove { opacity:1; }
  .drafts-card-remove:hover { background:var(--accent); }
  .drafts-card-piece { position:absolute; top:-6px; left:-6px; width:20px; height:20px; border-radius:50%; background:var(--text-soft); color:var(--canvas-bg); border:none; cursor:pointer; opacity:0; transition:opacity 0.12s; padding:0; display:flex; align-items:center; justify-content:center; }
  .drafts-card:hover .drafts-card-piece { opacity:1; }
  .drafts-card-piece:hover { background:var(--accent); }
  .drafts-card-piece svg { width:12px; height:12px; display:block; }
  .drafts-card-piece:disabled { opacity:0.4; cursor:default; }
  .drafts-card-flash { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:var(--accent); color:var(--canvas-bg); font-family:var(--font-sans); font-size:var(--font-6); padding:4px 10px; border-radius:999px; pointer-events:none; opacity:0; transition:opacity 0.2s; white-space:nowrap; z-index:5; box-shadow:0 2px 8px rgba(0,0,0,0.18); }
  .drafts-card-flash.show { opacity:1; }
  .drafts-import-wrap { position:relative; }
  .drafts-import-wrap input[type=file] { position:absolute; inset:0; opacity:0; cursor:pointer; }
  .resize-handle:hover, .resize-handle.dragging { background:var(--accent); opacity:0.4; }
  .project-drawer { position:fixed; top:0; left:0; width:320px; height:100vh; background:var(--panel); border-right:1px solid var(--border); z-index:100; transform:translateX(-100%); transition:transform 0.2s ease; display:flex; flex-direction:column; overflow:hidden; }
  .project-drawer.open { transform:translateX(0); }
  .project-drawer-header { padding:1.067rem 1.2rem; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:0.667rem; }
  .project-drawer-header h2 { font-family:var(--font-serif); font-size:22px; font-weight:var(--weight-regular); margin:0; display:flex; align-items:center; justify-content:space-between; }
  .project-actions { display:flex; gap:0.4rem; }
  .project-list { flex:1; overflow-y:auto; padding:0.533rem 0; }
  .project-list-item { padding:0.667rem 1.2rem; cursor:pointer; border-bottom:1px solid var(--border-light); transition:background 0.1s; }
  .project-list-item:hover { background:var(--bg); }
  .project-list-item.active { background:var(--bg); border-left:3px solid var(--accent); font-weight:var(--weight-medium); }
  .project-list-item-name { font-size:var(--font-2); font-weight:var(--weight-medium); color:var(--text); }
  .project-list-item-date { font-size:var(--font-5); color:var(--text-faint); margin-top:2px; }
  .project-list-item-drafts { font-size:var(--font-5); color:var(--text-soft); }
  .project-drawer-overlay { position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:99; display:none; }

  /* Collapsible sections */
  .section { border-bottom:1px solid var(--border-light); }
  .section-header {
    padding:0.667rem 1.2rem; cursor:pointer; display:flex; align-items:center; gap:0.533rem;
    font-size:var(--font-6); font-weight:var(--weight-medium); letter-spacing:0.12em; text-transform:uppercase;
    color:var(--text-faint); user-select:none; transition:background 0.1s;
  }
  .section-header:hover { background:var(--bg-warm); }
  .section-arrow { font-size:var(--font-5); transition:transform 0.15s; }
  .section.closed .section-arrow { transform:rotate(-90deg); }
  .section.closed .section-body { display:none; }
  .section-body { padding:0.4rem 1.2rem 0.933rem; display:flex; flex-direction:column; gap:0.533rem; }
  .section-right { margin-left:auto; font-weight:var(--weight-light); text-transform:none; letter-spacing:0; font-size:var(--font-5); }

  /* Buttons */
  .btn { font-family:var(--font-sans); font-size:var(--font-4); font-weight:var(--weight-regular); padding:0.333rem 0.667rem; border:1px solid var(--border); border-radius:var(--radius); background:var(--canvas-bg); color:var(--text); cursor:pointer; transition:all 0.12s; white-space:nowrap; }
  .btn:hover { background:var(--bg-warm); border-color:var(--accent-light); }
  .btn-icon { padding:0.2rem 0.4rem; font-size:var(--font-2); line-height:1; display:inline-flex; align-items:center; justify-content:center; }
  .btn-ghost { background:none; border-color:transparent; color:var(--text-soft); padding:0.2rem 0.333rem; }
  .btn-ghost:hover { color:var(--text); background:var(--bg-warm); border-color:var(--border); }
  .btn-add { padding:0.4rem 0; border-style:dashed; text-align:center; color:var(--text-soft); background:none; width:100%; }
  .btn-add:hover { border-color:var(--accent); color:var(--accent); }

  /* Batch-toolbar buttons (the "1 selected" action panel on Warp/Weft Cards)
     sit alongside .input-sm number fields. Match their vertical padding to
     the inputs so the row reads as a single strip of controls instead of
     tall buttons towering over short inputs. Font stays at .btn's font-4
     because Clone/Merge/Delete/etc. are real actions that deserve their
     prominence — only the padding shrinks. */
  [data-batch-toolbar] .btn { padding: 2px 6px; }

  /* Primary action button — for clear single-action CTAs like "Print
     Recipe" where the button is the final step of a workflow. Outlined
     accent on cream by default; fills with accent on hover. Heavier
     weight and slight letter-spacing make it read as deliberate rather
     than incidental. */
  .btn-big, .admin-save-btn { font-family:var(--font-sans); font-size:var(--font-4); font-weight:600; letter-spacing:0.5px; padding:0.6rem 1.25rem; border:1.5px solid var(--accent); border-radius:var(--radius); background:var(--canvas-bg); color:var(--accent); cursor:pointer; transition:all 0.12s; white-space:nowrap; }
  .btn-big:hover, .admin-save-btn:hover { background:var(--accent); color:#fff; }

  /* .btn-hero — the *filled* tier above .btn-big, for entry-point CTAs
     like the studio "Continue working" / "Begin a new draft" buttons. Same
     typography and proportions as .btn-big; the difference is fill.
     Filled accent at rest signals "front door" — this is what you came here
     to do — where .btn-big's outlined treatment fits in-context actions
     ("Print Dye Recipe") that follow other work. Hover deepens to
     --accent-hover so the button still responds, without changing its
     identity to outlined. */
  .btn-hero { font-family:var(--font-sans); font-size:var(--font-4); font-weight:600; letter-spacing:0.5px; padding:0.6rem 1.25rem; border:1.5px solid var(--accent); border-radius:var(--radius); background:var(--accent); color:#fff; cursor:pointer; transition:all 0.12s; white-space:nowrap; }
  .btn-hero:hover { background:var(--accent-hover); border-color:var(--accent-hover); }

  /* .btn-compact — the *small utility action* tier, sized for moves that
     support the work without competing for attention. Used for library
     imports/exports, palette tools, and similar housekeeping actions —
     things you reach for occasionally rather than constantly. Rounded
     lozenge with quiet faint text at rest; the border and text deepen on
     hover so the button responds without ever shouting. Visually quieter
     than .pill (whose role is filters and state toggles) and much smaller
     than .btn — sits at the bottom of the button hierarchy on purpose. */
  .btn-compact { font-family:var(--font-sans); font-size:var(--font-6); font-weight:var(--weight-regular); padding:3px 8px; border:1px solid var(--border-light); border-radius:9px; background:none; color:var(--text-faint); cursor:pointer; transition:all 0.12s; }
  .btn-compact:hover { background:var(--canvas-bg); color:var(--text); border-color:var(--accent-light); }
  .btn-compact:disabled { opacity:0.4; cursor:not-allowed; }
  .btn-compact:disabled:hover { background:none; color:var(--text-faint); border-color:var(--border-light); }
  /* When a .btn-compact lives inside a .drafts-import-wrap, an invisible
     file-input overlay (position:absolute; inset:0) sits on top to handle
     clicks. That overlay intercepts hover before it reaches the button, so
     the button's own :hover never fires. Mirror the hover state via the
     wrapper here so wrapped Import buttons feel as responsive as the bare
     Export button does. :not(:disabled) preserves disabled-state appearance. */
  .drafts-import-wrap:hover .btn-compact:not(:disabled) { background:var(--canvas-bg); color:var(--text); border-color:var(--accent-light); }
  .pill { font-family:var(--font-sans); font-size:var(--font-6); font-weight:var(--weight-regular); padding:2px 0.4rem; border:1px solid var(--border-light); border-radius:9px; background:none; color:var(--text-faint); cursor:pointer; transition:all 0.12s; }
  .pill:hover { background:var(--canvas-bg); color:var(--text); border-color:var(--accent-light); }
  .pill:disabled { opacity:0.4; cursor:not-allowed; }
  .pill:disabled:hover { background:none; color:var(--text-faint); border-color:var(--border-light); }
  .pill-active { background:var(--accent); color:#fff; border-color:var(--accent); }
  .pill-active:hover { background:var(--accent-hover); color:#fff; }
  /* Zone-card Commit: gentle tan fill with accent text/border at rest, lifting
     to white on hover (accent text/border held throughout). Quieter than the
     filled .pill-active but still clearly a button. The :hover must follow
     .pill:hover above to win on equal specificity — it keeps the accent text
     that .pill:hover would otherwise swap to --text. */
  .zone-commit { background:var(--bg-warm); color:var(--accent); border-color:var(--accent); }
  .zone-commit:hover { background:var(--canvas-bg); color:var(--accent); border-color:var(--accent); }
  /* Segmented control: a row of pills sharing a border, behaving as a
     single mutually-exclusive group. Outer container owns the border and
     rounded corners; inner pills drop their own borders and rely on a
     divider between them. Active pill fills with accent; the divider on
     either side of an active pill also gets the accent color so the fill
     extends seamlessly to its neighbors. */
  /* Mode toggle (Structure / Color): a recessed track with the active mode
     raised onto a near-white pill (border + shadow). Depth and contrast —
     not just a background tint — signal which mode is on, so it reads as a
     toggle rather than two same-looking buttons. */
  .mode-toggle {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    background: var(--bg-warm);
    border: 1px solid var(--border-light);
    border-radius: 9px;
    font-family: var(--font-sans);
  }
  .mode-toggle-opt {
    border: 1px solid transparent;
    border-radius: 7px;
    background: none;
    cursor: pointer;
    min-width: 4.5rem;
    padding: 5px 1rem;
    font-family: inherit;
    font-size: var(--font-5);
    font-weight: var(--weight-regular);
    color: var(--text-faint);
    transition: color 0.15s, background 0.15s, box-shadow 0.15s;
  }
  .mode-toggle-opt:not(.active):hover { color: var(--text-soft); }
  .mode-toggle-opt.active {
    background: var(--panel);
    border-color: var(--border);
    color: var(--text);
    font-weight: var(--weight-medium);
    box-shadow: 0 1px 2px rgba(0,0,0,0.10);
  }

  /* The cloth toolbar row uses one type size throughout (toggle, View, Crop,
     zoom, dimensions, Export, More). The base .pill is --font-6; scope the
     bump to #viewControls so pills elsewhere in the app keep their own size.
     The mode toggle, zoom label, and dimension readout are set to --font-5
     at their source, so this brings the pills into line with them. */
  #viewControls .pill { font-size: var(--font-5); }

  /* Cloth caption — the draft-name + setup line above the toolbar. Lives here
     rather than inline on the span so it's on the type scale and overridable.
     Name italic (a titled work), setup shorthand roman. render-info is the
     vestigial count span beside it; kept (JS still clears it) at a real scale
     size instead of the old undefined --font-md. */
  .cloth-caption { font-family:var(--font-sans); font-size:var(--font-4); font-weight:var(--weight-regular); color:var(--accent-light); }
  .cloth-caption-name { font-style:italic; }
  /* Save badge: the visible editor/drawer status. Roman (not italic) so it reads
     as metadata, not part of the title. "not saved" and "saved" stay quiet;
     "unsaved changes" warms to amber + medium weight — the one state inviting a Save. */
  .cloth-caption-badge { font-style:normal; white-space:nowrap; }
  .cloth-caption-badge::before { content:' · '; color:var(--accent-light); font-weight:var(--weight-regular); }
  .cloth-caption-badge-hidden { display:none; }
  .cloth-caption-badge-unsaved { color:var(--text-faint); }
  .cloth-caption-badge-saved   { color:#6f8a64; }
  .cloth-caption-badge-changes { color:#b3872b; font-weight:var(--weight-medium); }
  .render-info { font-size:var(--font-5); color:var(--text-soft); }
  /* Buttons that sit on the brown/accent background ("inverted" or
     "primary" surfaces) must keep their light text in every state. The
     generic .pill:hover repaints color to var(--text) — fine on the
     default near-white pill surface, but on a brown surface it collides
     with the background and reads as "missing text". The single rule
     below locks color to canvas-bg for all primary-surface variants and
     all interactive states, regardless of which more-specific selector
     also matches. New primary-style buttons should join this list rather
     than fix the symptom site-by-site. NOTE: only buttons whose SURFACE is
     the accent belong here. Outlined buttons like .btn-big own a light
     surface and carry accent-colored text at rest (flipping to light text
     only on hover, handled in their own rule) — locking them here would force
     the resting text light-on-light and make it vanish. Keep them out. */
  .pill-primary, .pill-primary:hover, .pill-primary:focus, .pill-primary:active,
  .pipe-btn, .pipe-btn:hover, .pipe-btn:focus, .pipe-btn:active {
    color: var(--canvas-bg);
  }
  .pipe-btn { font-family:var(--font-sans); font-size:var(--font-4); font-weight:var(--weight-regular); padding:0.533rem 1.067rem; border:1px solid var(--accent); border-radius:var(--radius); background:var(--accent); color:#fff; cursor:pointer; transition:all 0.12s; width:100%; text-align:center; margin-top:0.25rem; }
  .pipe-btn:hover { background:var(--accent-hover); }
  /* Armed state for two-step commit buttons — the button itself is the confirmation */
  .btn.armed, .pipe-btn.armed { background:#c74a3b; border-color:#a83a2d; color:#fff; font-weight:var(--weight-medium); animation:armedPulse 1.2s ease-in-out infinite; }
  .btn.armed:hover, .pipe-btn.armed:hover { background:#b33d30; border-color:#a83a2d; }
  @keyframes armedPulse {
    0%, 100% { box-shadow:0 0 0 0 rgba(199,74,59,0.35); }
    50% { box-shadow:0 0 0 4px rgba(199,74,59,0); }
  }

  /* Structure dropdown */
  .struct-select {
    font-family:var(--font-sans); font-size:var(--font-3); font-weight:var(--weight-regular);
    padding:0.4rem 0.667rem; border:1px solid var(--border); border-radius:var(--radius);
    background:var(--canvas-bg); color:var(--text); width:100%; outline:none;
    cursor:pointer; appearance:auto;
  }

  /* Palette */
  .palette-row { display:flex; gap:0.667rem; flex-wrap:wrap; align-items:flex-start; padding-bottom:0.533rem; }
  .palette-swatch { box-sizing:border-box; width:30px; height:30px; border-radius:3px; border:1px solid rgba(0,0,0,0.15); cursor:pointer; position:relative; transition:all 0.12s; margin-bottom:0.667rem; }
  .palette-swatch:hover { transform:scale(1.1); }
  .palette-swatch.selected { border-color:var(--text); box-shadow:0 0 0 1px var(--panel), 0 0 0 3px var(--accent); transform:scale(1.12); }
  .palette-swatch input[type="color"] { position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%; }
  .palette-swatch .swatch-idx { position:absolute; bottom:-19px; left:0; right:0; text-align:center; font-size:var(--font-5); font-weight:var(--weight-medium); color:var(--text-faint); cursor:pointer; }
  .palette-add { box-sizing:border-box; width:30px; height:30px; border-radius:3px; border:1px dashed var(--border); background:none; cursor:pointer; font-size:18px; color:var(--text-faint); display:flex; align-items:center; justify-content:center; vertical-align:top; padding:0; margin:0; }
  .palette-add:hover { border-color:var(--accent); color:var(--accent); }
  .palette-remove { position:absolute; top:-4px; right:-4px; width:14px; height:14px; border-radius:50%; background:var(--text-soft); color:#fff; font-size:var(--font-6); border:none; cursor:pointer; display:none; align-items:center; justify-content:center; line-height:1; }
  .palette-swatch:hover .palette-remove { display:flex; }

  /* Image drop */
  .image-drop { border:1px dashed var(--border); border-radius:var(--radius); padding:0.8rem; text-align:center; font-size:var(--font-4); color:var(--text-faint); cursor:pointer; position:relative; transition:all 0.12s; }
  .image-drop:hover { border-color:var(--accent-light); color:var(--accent); }
  .image-drop input { position:absolute; inset:0; opacity:0; cursor:pointer; }
  .image-preview { max-width:100%; border-radius:var(--radius); margin-top:0.4rem; }
  .extracted-colors { display:flex; gap:0.25rem; margin-top:0.4rem; flex-wrap:wrap; }
  .extracted-swatch { width:24px; height:24px; border-radius:3px; cursor:pointer; border:1px solid rgba(0,0,0,0.1); transition:transform 0.1s; }
  .extracted-swatch:hover { transform:scale(1.2); }

  /* Sequence generator */
  .seq-gen { display:flex; flex-direction:column; gap:0.533rem; }
  .seq-gen-row { display:flex; gap:0.4rem; align-items:center; font-size:var(--font-4); color:var(--text-soft); flex-wrap:wrap; }
  .seq-gen-row select { font-family:var(--font-sans); font-size:var(--font-4); padding:0.25rem 0.4rem; border:1px solid var(--border); border-radius:3px; background:var(--canvas-bg); color:var(--text); outline:none; min-width:90px; }
  .seq-gen-preview { display:flex; gap:0; flex-wrap:wrap; min-height:20px; }
  .seq-gen-cell { width:16px; height:16px; border:1px solid rgba(0,0,0,0.06); }
  .seq-gen-label { font-size:var(--font-5); color:var(--text-faint); }

  /* Optical mix */
  .mix-grid { display:grid; gap:0.4rem 0.9rem; grid-template-columns:1fr 1fr; }
  .mix-pair { display:flex; align-items:center; gap:0.4rem; font-size:var(--font-5); }
  .mix-swatch { width:24px; height:24px; border-radius:3px; border:1px solid rgba(0,0,0,0.08); flex-shrink:0; }
  /* Action buttons in the Studies panel (mix "<a> & <b> +", saved-palette
     "<name> +"): a solid .btn-compact — not the dashed palette-add box, which
     signals a colour picker. .mix-add keeps the mix label on one line; .btn-plus
     weights the trailing "+" up a touch as the action accent, shared across
     these buttons. */
  .mix-add { white-space:nowrap; flex-shrink:0; }
  .btn-plus { font-weight:700; font-size:1.1em; }
  /* Saved-palette apply button: name + the replace (circular-arrows) glyph,
     centred together. The icon sizes itself; this just lays them out. */
  .palette-apply { display:inline-flex; align-items:center; gap:0.4em; }
  .btn-replace-ico { display:inline-flex; flex-shrink:0; }
  .mix-label { color:var(--text-soft); font-size:var(--font-5); }

  /* Zones */
  .zones-list { display:flex; flex-direction:column; gap:0.4rem; }
  .zone-card { position:relative; background:var(--card); border:1px solid var(--border-light); border-radius:var(--radius); padding:0.533rem 0.6rem 0.267rem; display:flex; flex-direction:column; gap:0.333rem; transition:border-color 0.12s, box-shadow 0.12s; overflow:hidden; }
  /* Full-strength colour-sequence bar bled to the card's top edge. Negative
     margins cancel the card padding so it spans corner-to-corner; the card's
     overflow:hidden clips it to the rounded top. */
  .zone-color-bar { display:flex; height:10px; margin:-0.533rem -0.6rem 0; overflow:hidden; }
  .zone-card.collapsed .zone-color-bar { margin-top:-0.333rem; }
  /* Subtle thread-direction stripes. Warp cards get faint vertical lines
     (mirroring warp threads running up-and-down on the loom); weft cards
     get horizontal lines (the picks that lay across). The stripes are
     thin (1px every 6px) and very low contrast, so they read as texture
     rather than pattern — at a glance you can tell which column you're
     scrolling through without the cards feeling busy. */
  .zone-card[data-which="warp"] {
    background:
      repeating-linear-gradient(90deg, transparent 0, transparent 5px, rgba(0,0,0,0.035) 5px, rgba(0,0,0,0.035) 6px),
      var(--card);
  }
  .zone-card[data-which="weft"] {
    background:
      repeating-linear-gradient(0deg, transparent 0, transparent 5px, rgba(0,0,0,0.035) 5px, rgba(0,0,0,0.035) 6px),
      var(--card);
  }
  .zone-card.collapsed { padding:0.333rem 0.6rem 0; gap:0; }
  .zone-card:hover { border-color:var(--accent-light); }
  .zone-card.active, .zone-set.active { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
  .zone-set { border:1px solid var(--border-light); border-radius:6px; overflow:hidden; transition:border-color 0.12s, box-shadow 0.12s; }
  .zone-set:hover { border-color:var(--accent-light); }
  .struct-card { border:1px solid var(--border-light); transition:border-color 0.12s, box-shadow 0.12s; }
  .struct-card:hover { border-color:var(--accent-light); }
  .struct-card.is-set { border-color:var(--border-light); }
  .struct-card.active { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
  .zone-header { display:flex; flex-direction:column; gap:0.25rem; font-size:var(--font-4); }
  .zone-header-top { display:flex; align-items:center; gap:0.25rem; flex-wrap:wrap; }
  .zone-header-spacer { flex:1; min-width:0; }
  .zone-num { font-weight:var(--weight-medium); color:var(--text-soft); font-size:var(--font-5); opacity:0.85; margin-right:2px; }
  .zone-readout { font-size:var(--font-4); font-weight:var(--weight-regular); letter-spacing:0.04em; flex:1; }
  .zone-actions { display:contents; }
  .zone-actions > button:first-child { margin-left:auto; }
  .zone-threads { display:flex; gap:0; flex-wrap:wrap; }
  .thread-cell { flex:0 0 20px; width:20px; min-width:20px; height:20px; display:flex; align-items:center; justify-content:center; font-size:var(--font-7); font-weight:var(--weight-medium); user-select:none; border:1px solid rgba(0,0,0,0.06); box-sizing:border-box; }
  .thread-cell-add { flex:0 0 20px; min-width:20px; }
  .zone-bottom { display:flex; gap:0.25rem; align-items:center; flex-wrap:wrap; font-size:var(--font-5); color:var(--text-soft); }
  .zone-presets { display:flex; gap:2px; flex-wrap:wrap; }
  .zone-mode-row { display:flex; gap:0.2rem; }
  .zone-body-row { display:flex; gap:0.25rem; align-items:stretch; }
  .zone-body-content { flex:1; min-width:0; display:flex; flex-direction:column; gap:0.333rem; }
  .zone-move-arrows { display:flex; flex-direction:column; gap:1px; align-self:center; flex-shrink:0; }

  /* Structure panel sub-areas — Threading, Treadling, Tie-up */
  .struct-area { background:var(--card); border:1px solid var(--border-light); border-radius:var(--radius); padding:0.533rem 0.6rem; display:flex; flex-direction:column; gap:0.4rem; }
  .struct-area-title { font-size:var(--font-5); color:var(--text-faint); font-weight:var(--weight-medium); letter-spacing:0.04em; }

  /* View toggles */
  .view-toggle { display:flex; align-items:center; gap:0.533rem; font-size:var(--font-4); color:var(--text-soft); }
  .toggle-track { width:34px; height:18px; border-radius:9px; background:var(--border); cursor:pointer; position:relative; transition:background 0.15s; flex-shrink:0; }
  .toggle-track.on { background:var(--accent); }
  .toggle-knob { width:14px; height:14px; border-radius:50%; background:#fff; position:absolute; top:2px; left:2px; transition:left 0.15s; box-shadow:0 1px 3px rgba(0,0,0,0.15); }
  .toggle-track.on .toggle-knob { left:18px; }

  /* Main */
  .main { padding:15px 1.867rem; display:flex; flex-direction:column; align-items:center; gap:1.333rem; width:100%; flex:1; min-height:0; box-sizing:border-box; overflow:hidden; }
  /* The other tab wraps also need to flex-fill in the new layout. They
     already carry overflow:auto inline. */
  #loupeWrap, #studiesWrap, #studioWrap { flex:1; min-height:0; }
  .canvas-wrap { position:relative; overflow:auto; width:fit-content; max-width:100%; max-height:100%; }
  canvas { display:block; image-rendering:pixelated; }
  .render-info { font-size:var(--font-4); color:var(--text-soft); text-align:center; line-height:1.5; }

  /* Loading spinner — shown on initial cloth-pane load while JS bootstraps
     and runs the first render. Hidden by JS at the start of renderCanvas. */
  .spinner {
    width: 28px;
    height: 28px;
    border: 2.5px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: cloth-spinner-spin 0.8s linear infinite;
  }
  @keyframes cloth-spinner-spin {
    to { transform: rotate(360deg); }
  }

  /* Notation strip overlays (warpStripLetters, weftStripLetters) carry the
     letter labels as positioned children, plus a CSS-rendered grid that
     stays sharp at any zoom (independent of the canvas's internal pixel
     cap). Two stacked gradients give a hierarchy: faint per-cell scaffold
     beneath, stronger every-8 reference ticks on top — like ruler ticks.
     JS sets --cell-px to the display zoom (z) at render time, so gradient
     pitch always matches the cell size beneath. */
  #warpStripLetters, #weftStripLetters {
    --cell-px: 8px;
    /* Inset border via box-shadow: 1px ring at display resolution, no
       layout impact (unlike `border`, which would shift child positions).
       Replaces the canvas-drawn strokeRect outer border, which bloated
       under CSS scale on capped strips. */
    box-shadow: inset 0 0 0 1px rgba(0,0,0,0.4);
    background:
      /* every-8 reference: stronger lines at major intervals */
      repeating-linear-gradient(to right,
        transparent 0 calc(var(--cell-px) * 8 - 1px),
        rgba(0,0,0,0.30) calc(var(--cell-px) * 8 - 1px) calc(var(--cell-px) * 8)),
      repeating-linear-gradient(to bottom,
        transparent 0 calc(var(--cell-px) * 8 - 1px),
        rgba(0,0,0,0.30) calc(var(--cell-px) * 8 - 1px) calc(var(--cell-px) * 8)),
      /* per-cell: faint scaffold for cell-by-cell tracking */
      repeating-linear-gradient(to right,
        transparent 0 calc(var(--cell-px) - 1px),
        rgba(0,0,0,0.13) calc(var(--cell-px) - 1px) var(--cell-px)),
      repeating-linear-gradient(to bottom,
        transparent 0 calc(var(--cell-px) - 1px),
        rgba(0,0,0,0.13) calc(var(--cell-px) - 1px) var(--cell-px));
  }

  /* Recipe-style section container (used by Loupe analytical tables).
     Note: section titles now use .drawer-section-title; .recipe-section
     is just the wrapper that gives bottom-margin rhythm between tables. */
  .recipe-section { margin-bottom:1.2rem; }

  /* Snapshots — spans bottom grid row */
  .favorites-bar { grid-column:1/-1; background:var(--panel); border-top:1px solid var(--border); padding:0.533rem 1.067rem; display:flex; align-items:center; gap:0.533rem; overflow-x:auto; box-shadow:0 -2px 12px var(--shadow); }
  .favorites-bar:empty { display:none; }
  .favorites-bar.collapsed { justify-self:start; border-top:none; border-right:1px solid var(--border); border-top-right-radius:var(--radius-lg); padding:0.4rem 0.667rem; box-shadow:2px -2px 8px var(--shadow); }
  .favorites-bar.collapsed .snapshot-item, .favorites-bar.collapsed .favorites-label { display:none; }
  /* Actions bar — pinned inside center column */
  .actions-bar { background:var(--panel); border-top:1px solid var(--border); padding:0.4rem 1.067rem; display:flex; align-items:center; justify-content:center; gap:0.533rem; overflow-x:auto; flex-wrap:wrap; flex-shrink:0; }
  .favorites-label { font-size:var(--font-6); font-weight:var(--weight-medium); letter-spacing:0.1em; text-transform:uppercase; color:var(--text-faint); white-space:nowrap; margin-right:0.25rem; }
  .snapshot-thumb { width:60px; height:40px; border-radius:3px; border:1px solid var(--border); cursor:pointer; object-fit:cover; transition:filter 0.15s, border-color 0.12s, transform 0.12s; flex-shrink:0; }
  .snapshot-thumb:hover { border-color:var(--accent); transform:scale(1.08); }
  .snapshot-thumb.active { border:3px solid var(--accent); }
  .snapshot-thumb.dirty { filter:blur(2.5px); }
  .snapshot-item { position:relative; flex-shrink:0; }
  .snapshot-remove { position:absolute; top:-4px; right:-4px; width:14px; height:14px; border-radius:50%; background:var(--text-soft); color:#fff; font-size:var(--font-7); border:none; cursor:pointer; display:none; align-items:center; justify-content:center; line-height:1; }
  .snapshot-item:hover .snapshot-remove { display:flex; }
  .snapshot-item.dragging { opacity:0.4; }
  .snapshot-item.drop-before::before { content:''; position:absolute; left:-3px; top:0; bottom:0; width:2px; background:var(--accent); border-radius:1px; }
  .snapshot-item.drop-after::after { content:''; position:absolute; right:-3px; top:0; bottom:0; width:2px; background:var(--accent); border-radius:1px; }

  /* Drawer */
  .drawer-overlay { position:fixed; inset:0; background:rgba(42,38,35,0.15); z-index:90; opacity:0; pointer-events:none; transition:opacity 0.2s; }
  .drawer-overlay.open { opacity:1; pointer-events:auto; }
  .drawer { position:fixed; top:0; right:-360px; width:340px; height:100vh; background:var(--panel); border-left:1px solid var(--border); box-shadow:-4px 0 24px rgba(42,38,35,0.1); z-index:100; transition:right 0.25s ease; display:flex; flex-direction:column; }
  .drawer.open { right:0; }
  .drawer-wide { width:370px; right:-380px; }
  .drawer-wide.open { right:0; }
  .drawer-header { padding:0.933rem 1.2rem; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; }
  .drawer-title { font-family:var(--font-serif); font-size:20px; font-weight:var(--weight-regular); }
  .drawer-body { padding:1.2rem; overflow-y:auto; flex:1; display:flex; flex-direction:column; gap:1.333rem; }
  .drawer-section { display:flex; flex-direction:column; gap:0.533rem; padding:0.8rem; }
  .drawer-section-title { font-size:var(--font-5); font-weight:var(--weight-medium); letter-spacing:1px; text-transform:uppercase; color:var(--text-faint); }
  .drawer-subsection-title { font-size:var(--font-5); font-weight:600; color:var(--text-soft); }
  /* Setup tab form row — label sits adjacent to its input field, not
     across the row from it. (Earlier this used justify-content:
     space-between, which pushed label and input to opposite ends and made
     them visually disconnected; now they read as a pair.) */
  .calc-row { display:flex; align-items:center; gap:0.667rem; }
  .calc-row label { font-size:var(--font-4); color:var(--text-soft); }
  .calc-row .input { width: 80px; }
  .calc-result { font-size:var(--font-4); font-weight:var(--weight-light); font-style:italic; color:var(--text-soft); padding:0.4rem 0 2px; }
  .drawer-notes { font-family:var(--font-sans); font-size:var(--font-3); font-weight:var(--weight-light); padding:0.667rem; border:1px solid var(--border-light); border-radius:var(--radius); background:var(--canvas-bg); color:var(--text); outline:none; resize:vertical; min-height:100px; line-height:1.6; }
  .drawer-notes::placeholder { color:var(--text-faint); }

  /* Utility classes for high-frequency composed patterns. These let common
     visual roles (caption text, section headings, modal scrims) live in one
     place, so the responsive work has a vocabulary to retune at breakpoints
     instead of hunting through inline styles. Each class is a *role*, not a
     specific element — apply to any tag where the role fits.

     .ui-caption — small faint text used for help, labels, hints, deltas.
     Was the most-repeated inline pattern (~37 occurrences).

     Heading hierarchy — three tiers, all font-5:
     .drawer-section-title (defined with drawer styles) — weight 600,
                           uppercase, 1px tracking. Canonical PANEL
                           header for setup-tab and dye-lab sections AND
                           for column-level labels like the Notes pad.
     .drawer-subsection-title — weight 600, sentence case, no tracking.
                           Sub-header WITHIN a panel (e.g. "Depth of
                           shade" inside the Colors panel). Same size
                           and weight as the panel header; the lack of
                           uppercase + tracking signals subordination.
     .ui-section-heading — medium weight (500), uppercase, 1px tracking,
                           fainter color. A quieter sibling of
                           drawer-section-title for narrow contexts like
                           the right-side palette panel ("PALETTE",
                           "STRUCTURE"). */
  .ui-caption { font-size:var(--font-5); color:var(--text-faint); }
  .ui-section-heading { font-family:var(--font-sans); font-size:var(--font-5); font-weight:var(--weight-medium); color:var(--text-faint); text-transform:uppercase; letter-spacing:1px; }

  /* .item-name — sans headline treatment for the *name of a named thing*.
     Reserved for user-identifiable items: a color in the palette ("d · dark"),
     a material in the materials slot ("Lux Lace (Merino)"). At font-2 with
     medium weight it sits clearly above body text without leaning on a
     typeface change to distinguish itself — the size and weight do the
     work. Sub-data about the named thing (CMYK, grist, etc.) stays in
     sans-serif beneath at body-text scale. */
  .item-name { font-family:var(--font-sans); font-size:var(--font-2); font-weight:450; color:var(--text); line-height:1.2; }
  /* In the Setup materials card the yarn name sits below the draft name, which
     is now the screen's headline — so step the yarn down a size and weight
     here only. Elsewhere (palette/color labels) .item-name keeps its headline
     treatment. */
  #scratchMaterialsMount .item-name { font-size:var(--font-3); font-weight:var(--weight-regular); }

  /* .ui-modal-scrim — dim full-viewport backdrop for centered dialog
     content. The 0.3 alpha and z-index 1000 are the established values
     across the existing modals. */
  .ui-modal-scrim { position:fixed; inset:0; background:rgba(0,0,0,0.3); z-index:1000; display:flex; align-items:center; justify-content:center; }

  /* Sign-in link in the masthead. Sits alongside the UserButton mount;
     they take turns based on auth state. Quiet styling so it doesn't
     dominate the wordmark on the entrance. */
  .signin-link {
    font-family:var(--font-sans); font-size:var(--font-4);
    color:var(--text-soft); text-decoration:none;
    padding:0.4rem 0.8rem; border:1px solid var(--border); border-radius:4px;
    cursor:pointer; transition:all 0.15s; background:transparent;
  }
  .signin-link:hover { color:var(--accent); border-color:var(--accent); }

  /* Studio entrance: gating affordances when not signed in. The button
     is dimmed and shows a sign-in prompt on hover; clicking opens the
     sign-in modal rather than starting a draft. Structure cards stay
     visible (browsing is allowed) but get the same gated treatment. */
  .gated { opacity:0.55; cursor:pointer; }
  .gated:hover { opacity:0.75; }
  .gated::after {
    content: attr(data-gated-hint);
    display:none;
  }

  /* Input system — three sizes for text and number fields. Replaces six
     older patterns (.project-name-input, .zone-input, .zone-repeat-input,
     .calc-row input, .seq-gen-row input, and many inline style="..."
     strings in JS).

     Apply a size class on every input explicitly. Visual style (border,
     radius, background, focus, hover) is shared across sizes; each size
     class sets font-size, padding, and the typical text-alignment for
     that size's usual job. Override text-align inline when a large-size
     input is used for text rather than a measurement.

     .input-sm  — compact, centered. Inline numbers in cards/toolbars
                  (zone repeat, batch count, scale factor, group repeat).
     .input     — default, left-aligned. General text and form fields
                  (pattern code, color name, sentence, dialog text fields,
                  Setup tab measurements, dye lab weights). */
  .input-sm, .input {
    font-family: var(--font-sans);
    font-weight: var(--weight-regular);
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    outline: none;
    transition: border-color 0.12s ease;
    appearance: none;
    -webkit-appearance: none;
  }
  .input-sm:hover, .input:hover { border-color: var(--text-faint); }
  .input-sm:focus, .input:focus { border-color: var(--accent); }
  .input-sm::placeholder, .input::placeholder { color: var(--text-faint); font-style: italic; }

  .input-sm { font-size: var(--font-5); padding: 2px 6px;  text-align: center; }
  .input    { font-size: var(--font-4); padding: 4px 8px;  text-align: left;   }

  /* Number inputs: tabular figures across the board, native up/down steppers
     visible on every number field. The earlier design tried to strip steppers
     from larger labelled fields (.input) for a cleaner look, but the Setup
     fields are exactly where nudging matters most — EPI, draw-in %, fringe,
     loom reach — and the lost arrows made adjustment harder than typing. */
  input[type="number"].input-sm,
  input[type="number"].input { font-variant-numeric: tabular-nums; }

/* ═══════════════════════════════════════════════════════════════════════════
   STUDIO ENTRANCE (PHP-rendered page)
   ═══════════════════════════════════════════════════════════════════════════ */

.studio-page {
  max-width: 1100px;
  margin: 0 auto;
  padding: 4.267rem 2.133rem 6.4rem;
  font-family: var(--font-sans);
  color: var(--text);
}

.cover-plate {
  text-align: center;
  margin-bottom: 3.2rem;
  padding-bottom: 2.133rem;
  border-bottom: 1px solid var(--border);
}
.cover-icon {
  display: flex;
  justify-content: center;
  margin-bottom: 0.933rem;
}
.cover-title {
  font-family: var(--font-serif);
  font-size: 32px;
  font-weight: var(--weight-regular);
  color: var(--text);
  letter-spacing: 0.5px;
  margin: 0;
}
.cover-tagline {
  font-family: var(--font-sans);
  font-size: var(--font-3);
  color: var(--text-soft);
  font-style: italic;
  margin: 0.25rem 0 0;
}
.cover-beta-notice {
  font-family: var(--font-sans);
  font-size: var(--font-4);
  color: var(--text-faint);
  max-width: 580px;
  margin: 1.6rem auto 0;
  line-height: 1.55;
  text-align: center;
}
.cover-beta-notice a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.cover-beta-notice a:hover { color: var(--accent-hover); }

.begin-section {
  margin-bottom: 3.2rem;
  text-align: center;
}
.begin-buttons {
  display: flex;
  gap: 0.8rem;
  justify-content: center;
  flex-wrap: wrap;
}
/* Anchors styled as buttons need explicit color overrides since browsers
   default a:link/a:visited to blue/purple. */
.studio-page a.btn,
.studio-page a.btn:visited,
.studio-page a.btn:link { color: var(--text); text-decoration: none; }
.studio-page a.btn:hover { background: var(--bg-warm); }
/* .btn-big used as a landing-page anchor: browsers color links, so set the
   accent text explicitly at rest (matching the .btn-big component) and flip
   to white on hover. The accent fill on hover comes from .btn-big:hover. */
.studio-page a.btn-big,
.studio-page a.btn-big:visited,
.studio-page a.btn-big:link { color: var(--accent); text-decoration: none; }
.studio-page a.btn-big:hover { color: #fff; }

.section-heading {
  font-family: var(--font-sans);
  font-size: var(--font-5);
  font-weight: var(--weight-medium);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 1px;
  margin: 0 0 1.067rem;
}

/* — Structures gallery — */
.structures-section { margin-bottom: 3.2rem; }
.structures-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 1.067rem;
  align-items: start;
}
.structure-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.933rem;
}
/* Explicit "open in editor" action at the card's foot — the thumbnail above
   is also a link, but this names the action plainly. Full-width, centered. */
.structure-open-btn {
  display: block;
  margin-top: 0.667rem;
  text-align: center;
}
.structure-thumb-link {
  display: block;
  text-decoration: none;
  border-radius: 3px;
  overflow: hidden;
  transition: opacity 0.15s ease;
}
.structure-thumb-link:hover {
  opacity: 0.85;
}
.structure-thumb {
  width: 100%;
  height: 120px;
  overflow: hidden;
  display: block;
}
.structure-thumb svg {
  width: 100%;
  height: 100%;
  display: block;
  image-rendering: pixelated;
}

/* — Structure name + parts disclosure — */
/* The name itself is the disclosure trigger; clicking it reveals the
   threading, tie-up, and treadling of the structure. A small chevron
   rotates 90° on open. The thumbnail above is the action target (start
   a draft from this structure); the name is the info target. */
.structure-disclosure {
  margin-top: 0.667rem;
}
.structure-disclosure summary.structure-name {
  cursor: pointer;
  list-style: none;
  user-select: none;
  font-size: var(--font-4);
  color: var(--accent);
  font-weight: var(--weight-medium);
  display: flex;
  align-items: center;
}
.structure-disclosure summary::-webkit-details-marker { display: none; }
.structure-disclosure summary::before {
  content: '▸';
  display: inline-block;
  margin-right: 0.4em;
  font-size: 0.75em;
  transition: transform 0.15s ease;
  color: var(--text-faint);
}
.structure-disclosure[open] summary::before {
  transform: rotate(90deg);
}
.structure-disclosure summary:hover { color: var(--accent-hover); }
.structure-parts {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 0.667rem;
}
.part {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.part-label {
  font-size: var(--font-7);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: var(--weight-medium);
}
.part-viz {
  display: block;
  line-height: 0;
}
.part-viz svg {
  display: block;
  max-width: 100%;
  height: auto;
  image-rendering: pixelated;
}
.part-numbers {
  font-family: var(--font-mono);
  font-size: var(--font-5);
  color: var(--text);
  line-height: 1.4;
}

/* — Reference links — */
.reference-section { margin-bottom: 3.2rem; }
.reference-links {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.667rem;
}
.reference-link {
  font-size: var(--font-3);
  color: var(--text-soft);
}
.reference-link a {
  color: var(--accent);
  text-decoration: none;
  font-weight: var(--weight-medium);
}
.reference-link a:hover { text-decoration: underline; }
.reference-note {
  color: var(--text-faint);
  font-size: var(--font-4);
  margin-left: 0.533rem;
}

/* — About — */
.about-section { margin-bottom: 2.133rem; }
.about-body {
  font-size: var(--font-3);
  color: var(--text-soft);
  line-height: 1.6;
  max-width: 720px;
}
.about-body p { margin: 0 0 1em; }
.about-body p:last-child { margin-bottom: 0; }
.about-body strong, .about-body b { font-weight: var(--weight-medium); }
.about-body ul, .about-body ol { margin: 0 0 1em; padding-left: 1.4em; }
.about-body blockquote { margin: 0 0 1em; padding-left: 0.9em; border-left: 2px solid var(--border); }

/* — Signature: maker's mark + byline. Quiet, grounded, end-of-page. — */
.signature-section {
  margin-top: 4.267rem;
  display: flex;
  align-items: center;
  gap: 1.6rem;
}
.signature-mark-link {
  display: inline-flex;
  flex-shrink: 0;
  text-decoration: none;
  transition: opacity 0.15s;
}
.signature-mark-link:hover { opacity: 0.7; }
.signature-mark {
  width: 80px;
  height: auto;
  flex-shrink: 0;
  opacity: 0.9;
}
.signature-byline {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--font-3);
  color: var(--text-soft);
  margin: 0;
}

@media (max-width: 600px) {
  .studio-page { padding: 2.133rem 1.333rem 4.267rem; }
  .cover-title { font-size: 26px; }
  .structures-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
  .reference-note { display: block; margin-left: 0; margin-top: 2px; }
}

/* The cover plate previously had top padding to account for the fixed
   auth area. With the site bar above it, that's no longer needed. */
.studio-page { padding-top: 2.133rem; }

/* Home page corner ornament — a tree silhouette set as a quiet
   tail-piece in the bottom-right of the document (not the viewport),
   so it appears as readers reach the end of the page, the way an old
   book might have a printer's mark at the end of a chapter. The
   :has() selector scopes the rule to just the home page; sign-in,
   sign-up, account, and other pages stay clean.

   We render the watermark on a body::after pseudo-element (rather
   than as a body background-image) so we can apply CSS opacity —
   background-images can't be dimmed directly. The body needs
   position:relative for the absolute child to anchor to it; z-index:0
   on body and -1 on the pseudo-element keeps the ornament behind
   content. */
body:has(.studio-page) {
  position: relative;
  z-index: 0;
}
body:has(.studio-page)::after {
  content: '';
  position: absolute;
  bottom: 0;
  right: 0;
  width: 700px;
  height: 700px;
  background-image: url('./homepage-watermark.svg');
  background-position: bottom right;
  background-repeat: no-repeat;
  background-size: contain;
  opacity: 0.2;
  pointer-events: none;
  z-index: -1;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ADMIN PAGE
   Curator-only content editing. Form fields for tagline, About, links.
   ═══════════════════════════════════════════════════════════════════════════ */

.admin-page {
  width: 80%;
  margin: 0 auto;
  padding: 2.133rem 2.133rem 6.4rem;
  font-family: var(--font-sans);
  color: var(--text);
}
.admin-header { margin-bottom: 2.133rem; }
.admin-header h1 {
  font-family: var(--font-serif);
  font-size: 28px;
  font-weight: var(--weight-regular);
  color: var(--text);
  margin: 0 0 0.4rem;
}
.admin-intro {
  font-size: var(--font-4);
  color: var(--text-soft);
  margin: 0 0 1.6rem;
}
.admin-intro code {
  font-family: var(--font-mono);
  font-size: var(--font-5);
  background: var(--bg-warm);
  padding: 2px 0.4rem;
  border-radius: 3px;
}

.admin-flash {
  padding: 0.667rem 0.933rem;
  border-radius: 4px;
  font-size: var(--font-3);
  font-weight: var(--weight-medium);
  margin-bottom: 1.6rem;
}
.admin-flash-success {
  background: #e8f0e4;
  color: #4a6940;
  border: 1px solid #c4d4be;
}
.admin-flash-error {
  background: #f5e4e2;
  color: #8b3a32;
  border: 1px solid #d8b8b3;
}

/* Admin tabs — sit below the h1 in the header. Active tab carries a bottom
   border in the accent color; inactive tabs are quiet text. Anchored as <a>
   tags rather than buttons since each tab is a real URL (?tab=users). */
.admin-tabs {
  display: flex;
  gap: 1.6rem;
  margin-top: 1.067rem;
  border-bottom: 1px solid var(--border);
}
.admin-tab {
  font-family: var(--font-sans);
  font-size: var(--font-4);
  color: var(--text-soft);
  text-decoration: none;
  padding: 0.533rem 0;
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
}
.admin-tab:hover { color: var(--text); }
.admin-tab-active {
  color: var(--text);
  border-bottom-color: var(--accent);
}

/* Users table — minimal, readable. Numeric column right-aligned with tabular
   figures so the draft counts line up. Role badges are small pills, with the
   curator role getting a touch of accent color. */
.admin-users-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-sans);
  font-size: var(--font-4);
}
.admin-users-table th {
  text-align: left;
  font-weight: var(--weight-medium);
  color: var(--text-soft);
  font-size: var(--font-5);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding: 0.533rem 0.8rem;
  border-bottom: 1px solid var(--border);
}
.admin-users-table td {
  padding: 0.8rem;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  vertical-align: middle;
}
.admin-users-table tr:last-child td { border-bottom: none; }
.admin-users-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.admin-user-badge {
  display: inline-block;
  font-size: var(--font-5);
  padding: 2px 0.533rem;
  border-radius: 12px;
  background: var(--bg);
  color: var(--text-soft);
  border: 1px solid var(--border);
}
.admin-user-badge-curator {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.admin-users-empty {
  text-align: center;
  color: var(--text-faint);
  padding: 1.6rem;
}

.admin-form { display: flex; flex-direction: column; gap: 1.867rem; }
.admin-section {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 1.333rem 1.6rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
}
.admin-label {
  font-family: var(--font-sans);
  font-size: var(--font-5);
  font-weight: var(--weight-medium);
  color: var(--text);
  text-transform: uppercase;
  letter-spacing: 1px;
}
.admin-hint {
  font-size: var(--font-4);
  color: var(--text-faint);
  margin: 0 0 0.25rem;
}
.admin-input,
.admin-textarea {
  font-family: var(--font-sans);
  font-size: var(--font-3);
  color: var(--text);
  background: var(--canvas-bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.533rem 0.667rem;
  width: 100%;
  box-sizing: border-box;
  transition: border-color 0.15s;
}
.admin-input:focus,
.admin-textarea:focus {
  outline: none;
  border-color: var(--accent);
}
.admin-textarea {
  font-family: var(--font-sans);
  line-height: 1.5;
  resize: vertical;
}

/* — Reference link rows — */
.admin-links-list {
  display: flex;
  flex-direction: column;
  gap: 0.533rem;
  margin-top: 0.533rem;
}
.admin-link-row {
  display: grid;
  grid-template-columns: 24px 1fr 1.5fr 2fr 32px;
  gap: 0.533rem;
  align-items: center;
}
.admin-link-reorder {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.admin-link-move {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text-faint);
  font-size: 10px;
  cursor: pointer;
  width: 22px;
  height: 14px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  transition: all 0.15s;
}
.admin-link-move:hover {
  color: var(--accent);
  border-color: var(--accent);
}
.admin-link-row .admin-input {
  font-size: var(--font-4);
  padding: 0.4rem 0.533rem;
}
.admin-link-remove {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-faint);
  font-size: 16px;
  cursor: pointer;
  width: 32px;
  height: 32px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.15s;
}
.admin-link-remove:hover {
  color: #8b3a32;
  border-color: #d8b8b3;
}
.admin-add-btn {
  align-self: flex-start;
  font-family: var(--font-sans);
  font-size: var(--font-4);
  color: var(--accent);
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: 4px;
  padding: 0.4rem 0.8rem;
  cursor: pointer;
  margin-top: 0.8rem;
  transition: all 0.15s;
}
.admin-add-btn:hover {
  border-color: var(--accent);
  background: var(--bg-warm);
}

/* — Save / cancel actions — */
.admin-actions {
  flex-direction: row;
  align-items: center;
  gap: 1.067rem;
  background: transparent;
  border: none;
  padding: 0;
}
.admin-cancel-link {
  font-size: var(--font-4);
  color: var(--text-soft);
  text-decoration: none;
}
.admin-cancel-link:hover { color: var(--accent); }

/* Section subheads inside .admin-section blocks. Same voice as the rest
   of the app's section-level labels (.drawer-section-title in the editor,
   .section-heading on the home page): small uppercase, weight-medium,
   tracked, quietly coloured. Without this rule, h2.admin-subhead falls
   back to browser-default h2 styling (huge and chunky). */
.admin-subhead {
  font-family: var(--font-sans);
  font-size: var(--font-5);
  font-weight: var(--weight-medium);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 1px;
  margin: 0 0 0.667rem;
}

/* Body prose inside admin sections — keep paragraphs at the baseline
   reading size and let the section's flex gap handle spacing, except
   inside forms where the form's own structure controls layout. */
.admin-section > p {
  font-size: var(--font-3);
  color: var(--text);
  line-height: 1.5;
  margin: 0;
}
.admin-section > p.admin-hint {
  color: var(--text-faint);
  font-size: var(--font-4);
}
.admin-section strong {
  font-weight: var(--weight-medium);
}

@media (max-width: 600px) {
  .admin-page { width: 100%; padding: 1.6rem 1.067rem 4.267rem; }
  .admin-link-row {
    grid-template-columns: 24px 1fr 32px;
    grid-template-rows: auto auto auto;
  }
  .admin-link-row .admin-link-reorder { grid-column: 1 / 2; grid-row: 1; }
  .admin-link-row .admin-link-name { grid-column: 2 / 3; grid-row: 1; }
  .admin-link-row .admin-link-remove { grid-column: 3 / 4; grid-row: 1; }
  .admin-link-row .admin-link-url { grid-column: 1 / 4; grid-row: 2; }
  .admin-link-row .admin-link-note { grid-column: 1 / 4; grid-row: 3; }
}


  /* Verification banner — shown at the top of the editor (under the
     auth area) when the signed-in user hasn't verified their email.
     Soft yellow tint, brand-aware accent. Not modal; users can dismiss
     by verifying. */
  .verify-banner {
    background: rgba(255,210,0,0.18);
    border-bottom: 1px solid rgba(180,140,0,0.25);
    padding: 0.667rem 1.6rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.067rem;
    font-size: var(--font-5);
    color: var(--text);
  }
  .verify-banner-text { flex: 1; min-width: 0; }
  .verify-banner-form { margin: 0; }
  .verify-banner-btn {
    background: var(--accent);
    color: #fff;
    border: none;
    border-radius: 999px;
    padding: 0.4rem 0.933rem;
    font-size: var(--font-5);
    cursor: pointer;
    white-space: nowrap;
  }
  .verify-banner-btn:hover { background: var(--accent-hover); }

  /* ─────────────────────────────────────────────────────────────────────
     Stacked editor (≤999px). Narrower than the three-column desktop grid can
     hold, so we STACK the columns: tabs, then cloth, then the palette/loom
     panel, then the library. Applies all the way down — there's no lower
     floor, so the layout never reverts to the (broken-at-this-width) desktop
     grid. This is pure layout. The cloth keeps the normal desktop renderer,
     which is virtualized (it paints only the visible region into a
     viewport-sized canvas), so no draft length approaches the WebKit canvas
     cap. Real phones (<720px) are still redirected to the homepage on load
     by the head scripts in editor.php/index.php — that's a separate load-time
     gate, so a fresh phone never reaches this CSS, while a desktop window
     dragged narrow stays cleanly stacked rather than breaking.

     This block lives at the END of the stylesheet on purpose: it overrides
     the base editor grid rules by SOURCE ORDER, so it needs no !important.
     The old mobile block sat *before* the base rules and had to fight the
     cascade with !important — the mistake we deliberately don't repeat. */
  @media (max-width: 999px) {
    .app {
      display: flex;
      flex-direction: column;
      height: auto;
      overflow: visible;
    }
    .center-tabs   { order: 0; width: 100%; display: flex; justify-content: center; }
    .center-col    { order: 1; }
    .right-panel   { order: 2; width: 100%; }
    .drafts-rail   { order: 3; width: 100%; }
    .favorites-bar { order: 4; width: 100%; }
    .resize-handle { display: none; }

    /* The rail is full-width when stacked, but .drafts-panel inside it keeps
       its desktop 240px width unless we override it here — and a 240px panel
       leaves the grid below only enough room for one column, so the library
       stays a column no matter what the grid rule says. Free the panel to
       fill the rail and the grid gets its width. height:auto lets it grow with
       the page instead of trying to fill a now-undefined parent height. */
    .drafts-panel { width: 100%; height: auto; flex: none; }

    /* The virtualized renderer sizes the canvas to the cloth wrap's client
       height. Stacked, the wrap has no grid track to bound it — so pin the
       cloth column to a fraction of the viewport and let the cloth scroll
       inside. Without this the wrap grows to the full cloth height and the
       canvas with it, re-introducing the very cap problem virtualization
       exists to avoid. */
    .center-col { height: 68vh; min-height: 320px; }

    /* The sidebar show/hide toggles are meaningless when everything is
       stacked in one column — there are no sidebars to collapse — so hide
       both the panel toggle and the library toggle here. */
    .center-tabs .panel-toggle-btn { display: none; }
    .library-toggle { display: none; }

    /* The drafts rail is full-width when stacked, so a single column of cards
       wastes the horizontal space. Lay the other-draft cards out as a
       responsive grid instead. */
    .drafts-other-list {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
      gap: 0.6rem;
    }
  }

  /* ── Floating notepad ─────────────────────────────────────────────────────
     A draggable, resizable card that floats over any tab. position:fixed so it
     rides above the editor; weavery.js sets left/top (drag) and persists
     width/height (CSS resize) in S.notepad. Defaults to the bottom-right.
     Quill (snow) turns #notepadEditor into its .ql-container and inserts a
     .ql-toolbar as the previous sibling — so the panel's flex children become
     header → toolbar → editor. */
  #floatingNotepad {
    position: fixed; right: 24px; bottom: 24px;
    width: 360px; height: 420px;
    min-width: 240px; min-height: 200px;
    z-index: 900;
    display: flex; flex-direction: column;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 8px 30px rgba(0,0,0,0.18);
    overflow: hidden;
    resize: both;
  }
  .notepad-header {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: space-between;
    padding: 0.5rem 0.4rem 0.5rem 0.8rem;
    background: var(--bg-warm);
    border-bottom: 1px solid var(--border);
    cursor: move; user-select: none;
  }
  .notepad-title {
    font-family: var(--font-sans); font-size: var(--font-5);
    font-weight: var(--weight-medium); letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-soft);
  }
  .notepad-close {
    border: none; background: none; font-size: 18px; line-height: 1;
    color: var(--text-soft); cursor: pointer; padding: 0;
    width: 24px; height: 24px; border-radius: 4px;
    display: flex; align-items: center; justify-content: center;
  }
  .notepad-close:hover { background: var(--border); color: var(--text); }
  #floatingNotepad .ql-toolbar {
    flex: 0 0 auto; border: none; border-bottom: 1px solid var(--border);
    background: var(--panel);
  }
  #notepadEditor {
    flex: 1 1 auto; min-height: 0; overflow-y: auto;
    border: none; font-family: var(--font-sans); font-size: var(--font-4);
  }

  /* Active "Editing" card → Notes opener. A persistent (not hover-revealed)
     pill: it's the primary way into the notepad, so it stays visible on the
     card you're editing. Reflects open state. */
  .drafts-card-notes {
    display: flex; align-items: center; justify-content: center; gap: 5px;
    margin-top: 0.4rem; width: 100%; padding: 4px 8px;
    background: var(--bg-warm); border: 1px solid var(--border);
    border-radius: 4px; font-family: var(--font-sans); font-size: var(--font-6);
    color: var(--text-soft); cursor: pointer; transition: all 0.12s;
  }
  .drafts-card-notes:hover { border-color: var(--accent); color: var(--text); }
  .drafts-card-notes.open { background: var(--accent); border-color: var(--accent); color: var(--canvas-bg); }
  .drafts-card-notes svg { width: 13px; height: 13px; flex: 0 0 auto; }
  /* Non-active cards render the Notes button hidden, purely to reserve its row
     so cards stay the same height and switching the active draft doesn't shift
     the layout. */
  .drafts-card-notes-placeholder { visibility: hidden; }


/* ── Blog ─────────────────────────────────────────────────────────────────
   Public reading column + the admin Blog tab. Reuses the site tokens; titles
   in the serif (Cormorant), prose in the same spirit as .about-body. */
.blog-page { max-width: 760px; margin: 0 auto; padding: 3.5rem 1.5rem 6rem; }

.blog-index-head { margin-bottom: 2.5rem; }
.blog-index-title { font-family: var(--font-serif); font-weight: var(--weight-regular); font-size: 2.2rem; color: var(--text); margin: 0 0 0.3rem; }
.blog-index-sub { font-weight: var(--weight-light); font-size: var(--font-3); color: var(--text-faint); margin: 0; }
.blog-empty { color: var(--text-faint); font-size: var(--font-3); }

.blog-list { list-style: none; margin: 0; padding: 0; }
.blog-list-item { border-top: 1px solid var(--border-light); }
.blog-list-item:last-child { border-bottom: 1px solid var(--border-light); }
.blog-list-link { display: block; padding: 1.6rem 0; text-decoration: none; color: inherit; }
.blog-list-link:hover .blog-list-title { color: var(--accent); }
.blog-list-title { font-family: var(--font-serif); font-weight: var(--weight-regular); font-size: 1.6rem; color: var(--text); margin: 0 0 0.2rem; transition: color 0.12s; }
.blog-list-date { display: block; font-size: var(--font-6); font-weight: var(--weight-medium); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint); margin-top: 0.4rem; margin-bottom: 0.5rem; }
.blog-list-excerpt { font-size: var(--font-4); color: var(--text-soft); line-height: 1.55; margin: 0; }

.blog-back { display: inline-block; font-size: var(--font-5); color: var(--text-faint); text-decoration: none; margin-bottom: 1.8rem; }
.blog-back:hover { color: var(--accent); }
.blog-post-title { font-family: var(--font-serif); font-weight: var(--weight-regular); font-size: 2.4rem; line-height: 1.15; color: var(--text); margin: 0 0 0.4rem; }
.blog-post-date { font-size: var(--font-6); font-weight: var(--weight-medium); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint); margin: 0 0 2.2rem; }

.blog-prose { font-size: var(--font-3); color: var(--text); line-height: 1.7; }
.blog-prose p { margin: 0 0 1.1em; }
.blog-prose p:last-child { margin-bottom: 0; }
.blog-prose strong, .blog-prose b { font-weight: var(--weight-medium); }
.blog-prose em, .blog-prose i { font-style: italic; }
.blog-prose h1, .blog-prose h2, .blog-prose h3 { font-family: var(--font-serif); font-weight: var(--weight-medium); color: var(--text); line-height: 1.25; margin: 1.6em 0 0.5em; }
.blog-prose h1 { font-size: 1.7rem; }
.blog-prose h2 { font-size: 1.45rem; }
.blog-prose h3 { font-size: 1.2rem; }
.blog-prose ul, .blog-prose ol { margin: 0 0 1.1em; padding-left: 1.4em; }
.blog-prose li { margin: 0 0 0.3em; }
.blog-prose blockquote { margin: 1.4em 0; padding-left: 1em; border-left: 2px solid var(--border); color: var(--text-soft); font-style: italic; }

/* Admin: Blog tab */
.post-meta-row { display: flex; align-items: flex-end; gap: 1.5rem; margin-top: 1rem; flex-wrap: wrap; }
.post-meta-date { flex: 0 0 auto; }
.post-meta-date .admin-input { width: auto; }
.post-published-toggle { display: inline-flex; align-items: center; gap: 0.5rem; font-size: var(--font-4); color: var(--text-soft); cursor: pointer; padding-bottom: 0.4rem; }
.post-published-toggle input { width: 1rem; height: 1rem; }

.admin-posts-table { width: 100%; border-collapse: collapse; }
.admin-posts-table td { padding: 0.7rem 0.5rem; border-top: 1px solid var(--border-light); vertical-align: middle; }
.admin-posts-table tr:first-child td { border-top: none; }
.post-row-title a { color: var(--text); text-decoration: none; font-weight: var(--weight-medium); }
.post-row-title a:hover { color: var(--accent); }
.post-row-date { color: var(--text-faint); font-size: var(--font-5); white-space: nowrap; }
.post-row-status { white-space: nowrap; }
.post-row-actions { text-align: right; white-space: nowrap; }
.post-action-link { color: var(--accent); text-decoration: none; font-size: var(--font-5); margin-right: 0.9rem; }
.post-action-link:hover { text-decoration: underline; }
.post-delete-form { display: inline; }
.post-action-delete { background: none; border: none; color: var(--text-faint); font-size: var(--font-5); cursor: pointer; padding: 0; font-family: inherit; }
.post-action-delete:hover { color: #a8362a; }
