/* ---------------------------------------------------------------------------
   Design tokens (NAOC-116)

   LIGHT is the default and lives on :root; dark is an override keyed on
   data-theme. That direction matters: an unthemed page, a page whose script
   failed, and a print stylesheet all fall back to :root, and light is the
   safer thing to land on in each case.

   Everything below is either a SURFACE (changes with the theme) or a MEANING
   (danger, warning, an axis colour). Surfaces flip wholesale. Meanings keep
   their hue but change lightness, because a colour picked to read on a dark
   panel does not read on a white one -- gold at #e8c547 on white is close to
   invisible, so light mode darkens it rather than keeping "the same" colour.

   The 3D viewport's background, grid and ground light are NOT here: they are
   user settings driving a WebGL scene (see src/settings.js). src/theme.js
   holds their per-theme defaults so the two stay in step.
--------------------------------------------------------------------------- */
:root {
  color-scheme: light;

  /* surfaces */
  --bg: #eef1f6;              /* app background / canvas surround */
  --panel: #ffffff;           /* sidebar and panel faces */
  --surface-raised: #ffffff;  /* popovers and floating panels, above a panel */
  --field: #f4f6fa;           /* inset wells: inputs, thumbnails */
  --control: #eef1f6;         /* button faces */
  --control-hover: #e3e8f0;
  --border: #d8dde6;
  --hover: rgba(0, 0, 0, 0.05);        /* subtle row/button hover tint */
  --shadow: rgba(15, 23, 42, 0.16);

  /* text */
  --text: #1f2430;
  --text-strong: #0b0e14;     /* maximum contrast, e.g. an active icon */
  --muted: #6b7280;

  /* hover tips (NAOC-336). Dark on light and light on dark, which is what a
     native tooltip does and therefore what a reader expects. */
  --tip-bg: #2b2f38;
  --tip-fg: #f2f3f7;
  --tip-border: #3d424e;

  /* over the canvas: chips and labels that float on the workspace */
  --scrim: rgba(255, 255, 255, 0.82);
  --scrim-strong: rgba(255, 255, 255, 0.92);
  --on-canvas: #1f2430;
  --on-canvas-shadow: rgba(255, 255, 255, 0.9);

  /* meanings */
  --accent: #3b6fe0;
  --accent-soft: rgba(59, 111, 224, 0.12);
  --accent-tint: rgba(59, 111, 224, 0.25);
  --danger: #d64545;
  --danger-soft: #b93b3b;
  --on-danger: #ffffff;
  --warn: #a97706;

  /* gizmo axes -- these match the 3D handles, so they are the same colour in
     both themes by definition; only their lightness is nudged for contrast */
  --axis-x: #d64545;
  --axis-y: #d18416;
  --axis-z: #3b6fe0;

  /* transparency checkerboard (paint canvas) */
  --checker: #e2e6ec;
  --checker-base: #f7f9fc;
  /* The desk the page sits on. Deliberately its own token rather than --bg:
     the surround's whole job is to NOT match the page, and in dark mode --bg
     and --checker-base are the same colour, which is exactly how the page came
     to be invisible. */
  --canvas-surround: #c9d1de;
  --page-edge: #9aa5b8;
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #1b1d22;
  --panel: #24272e;
  --surface-raised: #2c2f38;
  --field: #171920;
  --control: #34384a;
  --control-hover: #3d4257;
  --border: #383c46;
  --hover: rgba(255, 255, 255, 0.08);
  --shadow: rgba(0, 0, 0, 0.45);

  --text: #e8e9ee;
  --text-strong: #ffffff;
  --muted: #9aa0ac;

  --tip-bg: #f2f3f7;
  --tip-fg: #1f2430;
  --tip-border: #cbd0da;

  --scrim: rgba(0, 0, 0, 0.55);
  --scrim-strong: rgba(20, 22, 28, 0.8);
  --on-canvas: #e8e9ee;
  --on-canvas-shadow: rgba(0, 0, 0, 0.9);

  --accent: #5b8cff;
  --accent-soft: rgba(91, 140, 255, 0.12);
  --accent-tint: rgba(91, 140, 255, 0.25);
  --danger: #ff5c5c;
  --danger-soft: #ff8080;
  --on-danger: #1b1d22;
  --warn: #e8c547;

  --axis-x: #ff5c5c;
  --axis-y: #ffb454;
  --axis-z: #5b8cff;

  --checker: #2a2d36;
  --checker-base: #1b1d22;
  /* Darker than the page, not lighter: the artwork should read as lit and the
     desk as the hole around it, the same way it does in the light theme. */
  --canvas-surround: #0e0f13;
  --page-edge: #4d5361;
}
* { box-sizing: border-box; }
html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 14px;
}
#app {
  display: grid;
  grid-template-columns: 300px 1fr;
  /* The bar is its own row spanning both columns, so the sidebar and the
     canvas both start below it. `auto` rather than a fixed height: the bar
     sizes to its contents, which is what keeps it slim without pinning a
     number that the contents can then outgrow. */
  grid-template-rows: auto 1fr;
  height: 100%;
  min-height: 0;
}

/* ---------- top bar (NAOC-116 pass 2) ----------
   Document-level controls, across all three tools. Everything here belongs to
   the FILE (its name, its history, its menu) or to the app (which tool, which
   theme) -- never to what you are currently drawing with, which stays in the
   left palette, or to the current selection, which floats on the canvas. */
#topbar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 12px;
  height: 48px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.topbar-brand {
  font-size: 13px;
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--text);
  text-decoration: none;
  padding-right: 4px;
  flex: none;
}
/* The name field looks like plain text until you go near it -- Tinkercad's
   trick, and the reason its title does not read as a form. The affordance
   appears on hover and focus, when you are actually asking for it. */
.topbar-name {
  flex: 0 1 220px;
  min-width: 90px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 5px 8px;
  font-size: 13px;
  font-family: inherit;
  color: var(--text);
}
.topbar-name:hover { background: var(--control); }
.topbar-name:focus {
  outline: none;
  background: var(--field);
  border-color: var(--accent);
}
.topbar-name-static { color: var(--muted); }
.topbar-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
/* Adopted buttons stop being full-width sidebar blocks and become bar-sized.
   The alternative -- editing every call site -- would have meant three pages
   of churn to change one thing about where they live. */
#topbar button.action {
  width: auto;
  padding: 6px 12px;
  font-size: 12px;
}
.topbar-spacer { flex: 1 1 auto; }
.topbar-switcher {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  background: var(--field);
  border: 1px solid var(--border);
  border-radius: 8px;
  flex: none;
}
.topbar-tool {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: 6px;
  font-size: 12px;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}
.topbar-tool:hover { background: var(--hover); color: var(--text); }
.topbar-tool.active {
  background: var(--panel);
  color: var(--text);
  font-weight: 600;
  box-shadow: 0 1px 2px var(--shadow);
  cursor: default;
}
.topbar-tool svg { width: 16px; height: 16px; flex: none; }
.topbar-icon-btn {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  background: transparent;
  color: var(--muted);
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
}
.topbar-icon-btn:hover { background: var(--control); color: var(--text); }
.topbar-icon-btn svg { width: 18px; height: 18px; display: block; }

/* The sign-in control. Hidden entirely on a local install, where there is
   nobody to sign in as, rather than shown greyed out -- a disabled control
   invites somebody to work out how to enable it, and here there is nothing
   to enable. The one time it IS shown disabled is a shared server missing
   its config, and then the title says which setting. */
.topbar-save-state { flex: none; font-size: 12px; color: var(--muted); white-space: nowrap; margin-left: -4px; }
.topbar-save-state.error { color: var(--danger-soft); }
.topbar-signin-slot { flex: none; display: flex; align-items: center; position: relative; }
/* The account menu opens from the right-hand end of the bar, so it hangs
   leftward from the control rather than off the edge of the window. */
.menu-popover.account-menu { left: auto; right: 0; min-width: 160px; }
.menu-popover.account-menu button.danger { color: var(--danger-soft); }
.menu-popover.account-menu button.danger:hover { background: var(--danger); color: white; }
.menu-popover.account-menu { max-width: 240px; }
.menu-popover .menu-note { padding: 6px 10px 4px; font-size: 11px; line-height: 1.45; color: var(--muted); border-top: 1px solid var(--border); margin-top: 4px; }
.menu-popover .menu-note a { color: var(--accent); }
/* The plan line leads the signed-in menu: a heading for it, not a footnote. */
.menu-popover .menu-note:first-child { border-top: none; margin-top: 0; padding-top: 4px; color: var(--text); font-weight: 600; }
.topbar-signin {
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 5px 10px;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--control);
  border-radius: 6px;
  font: inherit;
  cursor: pointer;
}
.topbar-signin:hover:not(:disabled) { background: var(--control); color: var(--text); }
.topbar-signin:disabled { cursor: default; opacity: 0.6; }

/* The "this is only in your browser" line (NAOC-359). Quiet enough not to
   shout on every visit, present enough to be read, and never a tooltip.

   A TINTED BLOCK RATHER THAN A BORDER, and that is not a style preference.
   The first version drew a 1px border in var(--control), which is #eef1f6 --
   against the popover's white that is a contrast ratio of about 1.05:1, so
   it was computed, applied, and invisible. Looking at it is what found that;
   nothing about the CSS looked wrong. The same token as a FILL separates the
   block properly, and works in both themes because it is defined per theme.

   AND var(--text), NOT var(--muted). Muted on the tinted block measured
   4.42:1 in the dark theme, which is under WCAG AA's 4.5 for body text at
   this size; --text measures 9.57. This particular sentence is the one
   telling somebody their work can disappear, so it is the last thing that
   should be hard to read. The smaller size keeps it quiet without hiding it,
   which is what --muted was being asked to do and should not have been. */
.local-notice {
  font-size: 12px;
  line-height: 1.45;
  color: var(--text);
  padding: 8px 10px;
  margin-bottom: 6px;
  border-radius: 6px;
  background: var(--control);
}

/* Below ~1000px the tool names would push the bar into wrapping; the icons
   still say which is which, and the title attribute still names them. */
@media (max-width: 1000px) {
  .topbar-tool span { display: none; }
  .topbar-tool { padding: 5px 8px; }
}
#panel {
  background: var(--panel);
  border-right: 1px solid var(--border);
  padding: 14px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* ---------- left palette (NAOC-116 pass 3) ----------
   Two zones, and the split is the point. PINNED holds what you reach for
   constantly -- the tools and the actions -- so it never scrolls away and
   never hides behind a tab. Below it is CONTENT you browse (shapes, the
   scene tree, saved assets), and only one of those is open at a time.

   Before this the column stacked eight fieldsets and scrolled, which is why
   spacing alone would not have made it feel roomier: the problem was how
   much was open at once, not how tightly it was packed. */
.palette-pinned {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.palette-label {
  color: var(--muted);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.palette-label + .icon-toolbar { margin-bottom: 4px; }

/* A sentence of explanation under a palette-label. Not #panel .subtitle:
   these panels get MOVED into the floating context panel at runtime, so a
   selector rooted at #panel stops matching the moment they are adopted. */
.palette-hint {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.4;
  margin: 2px 0 6px;
}

/* A declared parameter's hint. Same look as .palette-hint, which is what these
   were written as by hand before the runtime drew them. */
/* Why a control is greyed, under the control (NAOC-316). Reads like a hint --
   it is the same kind of aside -- but carries a warning tint, because a hint
   describes a control that works and this one explains one that does not. */
.declared-unavailable:empty { display: none; }
.declared-unavailable {
  color: var(--warn, #a2701c);
  font-size: 11px;
  line-height: 1.4;
  margin: -2px 0 6px;
}

.declared-hint {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.4;
  margin: -2px 0 6px;
}

/* Tab labels for the three content panels. */
.palette-tabs {
  display: flex;
  flex: none;
  gap: 2px;
  padding: 3px;
  background: var(--field);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.palette-tab {
  flex: 1;
  padding: 6px 4px;
  background: transparent;
  color: var(--muted);
  border: none;
  border-radius: 6px;
  font-size: 12px;
  font-family: inherit;
  cursor: pointer;
}
.palette-tab:hover { color: var(--text); }
.palette-tab.active {
  background: var(--panel);
  color: var(--text);
  font-weight: 600;
  box-shadow: 0 1px 2px var(--shadow);
}
/* The open panel takes the rest of the column and scrolls on its own, so a
   long asset list never pushes the tabs off the top. */
.palette-content {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
#panel h1 {
  font-size: 15px;
  margin: 0 0 4px;
}
/* THE MODE HINT CAN CARRY TWO THINGS: why the tool changed (NAOC-244) and
   what the tool does. pre-line puts the first on its own line without needing
   a second element, so the explanation is not swallowed by the paragraph after
   it. Selected by ID rather than through #panel deliberately: these panels are
   MOVED into the floating context panel at runtime, and a selector rooted at
   #panel stops matching the moment they are adopted, which is the trap the
   note above .palette-hint records. Existing hints carry no newlines, so this
   changes nothing about how they render. */
#deformHint { white-space: pre-line; }

#panel .subtitle {
  color: var(--muted);
  font-size: 12px;
  margin: 0 0 8px;
}
#dropzone {
  border: 2px dashed var(--border);
  border-radius: 8px;
  padding: 20px 12px;
  text-align: center;
  color: var(--muted);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
#dropzone:hover, #dropzone.dragover {
  border-color: var(--accent);
  color: var(--text);
}
#dropzone.dragover {
  background: var(--accent-soft);
}
#dropzone img {
  max-width: 100%;
  max-height: 100px;
  display: block;
  margin: 0 auto 8px;
  border-radius: 4px;
}
#fileInput { display: none; }

fieldset {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px 12px;
  margin: 0;
}
legend {
  color: var(--muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0 4px;
}

/* Settings panel -- a native <details> disclosure (collapsed by default)
   styled to match the fieldset/legend look everything else in the panel
   uses, since it's the same "labeled group of controls" shape. */
#settingsPanel {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px 12px;
}
#settingsPanel summary {
  color: var(--muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
}
#settingsPanel[open] summary { margin-bottom: 10px; }
#settingsPanel .row:first-of-type { margin-top: 2px; }
input[type="color"] {
  width: 44px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: transparent;
  cursor: pointer;
}
.row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
}
.row:last-child { margin-bottom: 0; }
.row label {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: var(--text);
}
.row label .val {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
input[type="range"] { width: 100%; accent-color: var(--accent); }

.val-input {
  width: 58px;
  background: var(--field);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 6px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.val-input:focus {
  outline: none;
  border-color: var(--accent);
}

/* Text/select fields that need full width and left-aligned content, unlike
   .val-input's narrow right-aligned numeric fields (Add Text panel). */
.text-field {
  width: 100%;
  background: var(--field);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 5px 7px;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}
input.text-field { cursor: text; }
.text-field:focus {
  outline: none;
  border-color: var(--accent);
}
.val-unit {
  color: var(--muted);
  margin-left: 4px;
}

.mode-toggle {
  display: flex;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.mode-toggle button {
  flex: 1;
  background: transparent;
  color: var(--muted);
  border: none;
  padding: 8px;
  font-size: 12px;
  cursor: pointer;
}
.mode-toggle button.active {
  background: var(--accent);
  color: white;
}

/* Small icon-only tool buttons -- paint.html's Tool panel specifically
 * (deliberately a separate class from .mode-toggle, which stays untouched
 * since editor.html/index.html reuse it for their own full-width text
 * buttons like "Move"/"Rotate"). Square, fixed-size, wraps if a row ever
 * runs out of room, and reuses the same border/accent language as every
 * other button in the app. */
.icon-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
/* Fixed-column variant. Free wrapping is right when there are enough tools
 * to fill every row (paint.html), but a count that divides badly into the
 * panel width leaves a stranded row of one or two, which reads as a
 * rendering accident rather than a layout. Pinning the column count makes
 * every row the same width, so the block looks deliberate. */
.icon-toolbar.icon-grid {
  display: grid;
  /* Sized from the button rather than a repeated literal, so growing the
     buttons cannot leave the grid columns behind. */
  grid-template-columns: repeat(var(--cols, 4), 36px);
  justify-content: start;
  gap: 6px;
}
.icon-toolbar button {
  width: 36px;
  height: 36px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}
.icon-toolbar button:hover:not(.is-disabled) { border-color: var(--accent); color: var(--text); }
/* Unavailable icon buttons are greyed with a CLASS, not the `disabled`
 * attribute. Browsers suppress the tooltip on a disabled control, and for an
 * icon-only button that tooltip is the only label there is -- greying one
 * out would leave an unexplained glyph. Keeping it hoverable also lets the
 * tooltip say what the action needs ("Group -- select 2 or more objects"),
 * which the old popover, greying a text label, could not. */
.icon-toolbar button.is-disabled { opacity: 0.35; cursor: not-allowed; }
.icon-toolbar button.active {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
.icon-toolbar button svg { width: 19px; height: 19px; display: block; }

.primitive-category { margin-bottom: 12px; }
.primitive-category:last-child { margin-bottom: 0; }
.primitive-category-label {
  color: var(--muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 6px;
}
.primitive-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.primitive-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 10px 4px;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 12px;
  cursor: pointer;
}
.primitive-btn:hover { border-color: var(--accent); color: var(--text); }
.primitive-thumb {
  width: 34px;
  height: 34px;
  display: block;
}

/* Saved color swatches (paint.html's Color panel). Modeled on
   .primitive-grid above -- same small-clickable-grid language -- but each
   button IS its color, so no icon/label inside and no transparent
   background to override on hover. */
.swatch-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 4px;
}
.swatch {
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
}
.swatch:hover { border-color: var(--accent); }
.swatch-empty {
  color: var(--muted);
  font-size: 12px;
}

.checkbox-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
}
.checkbox-row input[type="checkbox"] { accent-color: var(--accent); }
.checkbox-row label { flex: 1; }
.checkbox-row + .checkbox-row { margin-top: 8px; }

button.action {
  background: var(--control);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 9px 10px;
  font-size: 12px;
  cursor: pointer;
  width: 100%;
}
button.action:hover { border-color: var(--accent); }
button.action:disabled { opacity: 0.4; cursor: not-allowed; }
button.action.active {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
#holeToggleBtn.active {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--on-danger);
}

.export-row {
  display: flex;
  gap: 8px;
}

/* Reusable dropdown menu button (Project ops, Selection actions) -- a
   button.action that toggles a small absolutely-positioned command list
   instead of navigating or submitting. See buildMenu() in editorMain.js. */
.menu-button {
  position: relative;
}
.menu-popover {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  /* Sized by its CONTENT, with the button's width as a floor. `right: 0` tied
     it to the button instead, so a label longer than "Project" wrapped -- and
     "Export 3MF (colour)" wrapped onto three lines, which reads as three
     items. A menu item is a single line or it is not one item. */
  right: auto;
  width: max-content;
  min-width: 100%;
  max-width: 260px;
  /* ABOVE .context-panel, which is also positioned over the viewport. Both sat
     at 20, so DOM order decided -- and the context panels are created at
     runtime, so they won an argument nobody knew was happening. The Project
     menu opens downward from the top bar straight into the top-left dock, so
     with a tool active its lower items (the Export entries among them) were
     drawn underneath the panel.

     A menu is transient and deliberately summoned; a panel is ambient. The
     transient thing wins, always, or the menu is unusable exactly when a tool
     is open -- which is most of the time. */
  z-index: 30;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px;
  margin: 0;
  list-style: none;
  box-shadow: 0 8px 24px var(--shadow);
}
.menu-popover li { margin: 0; }
.menu-popover button {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  color: var(--text);
  border: none;
  border-radius: 4px;
  padding: 8px 10px;
  font-size: 12px;
  cursor: pointer;
}
.menu-popover button:hover:not(:disabled) { background: var(--accent); color: white; }
.menu-popover button:disabled { color: var(--muted); opacity: 0.5; cursor: not-allowed; }
.menu-popover button.active { color: var(--danger-soft); }

/* "Open..." picker popover for saved Projects/Documents (editorMain.js/
   paintMain.js) -- unlike the plain New/Save/Save As menu items above,
   each row needs two independent click targets (open vs delete), so these
   rows aren't single full-width <button>s like the rest of .menu-popover. */
.picker-list {
  max-height: 220px;
  overflow-y: auto;
  min-width: 240px;
}
.menu-popover li.picker-row {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0;
}
.menu-popover .picker-open {
  flex: 1;
  width: auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.menu-popover .picker-open .picker-name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.menu-popover .picker-open .picker-date {
  color: var(--muted);
  font-size: 10px;
}
.menu-popover .picker-delete {
  flex: 0 0 auto;
  width: auto;
  color: var(--danger-soft);
}
.picker-empty {
  color: var(--muted);
  font-size: 12px;
  padding: 8px 10px;
}

/* Native <select>s used for the mode/category pickers -- styled to match
   button.action so they read as part of the same control family. */
select.action {
  background: var(--control);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 9px 10px;
  font-size: 12px;
  cursor: pointer;
  width: 100%;
}
select.action:hover { border-color: var(--accent); }

#stats {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.5;
}

#viewport {
  position: relative;
}
#viewport canvas { display: block; width: 100%; height: 100%; }

/* paint.html's canvas is sized/panned/zoomed by paintMain.js itself (CSS
   px width/height + a translate/scale transform), not stretched to fill
   #viewport like the 3D renderer's canvas above -- this overrides the
   generic #viewport canvas rule for it specifically. */
#paintViewport {
  position: absolute;
  inset: 0;
  overflow: hidden;
  cursor: crosshair;
  background: var(--canvas-surround);
  /* WITHOUT THIS A FINGER OR PEN STROKE ENDS AFTER ITS SECOND MOVE. The
     browser owns touch gestures unless told otherwise: after a few pixels
     of drag it decides this is a scroll, fires pointercancel, and the stroke
     handler never hears another move. Measured through Chromium's real
     touch pipeline before the fix: 60 moves sent, 2 delivered, 1 cancel,
     one dab on the canvas. A mouse never hits this, which is why the suite
     could not see it. */
  touch-action: none;
}
/* The checkerboard means "this part of the ARTWORK is transparent", so it
   belongs to the page and not to the viewport around it. It used to be on
   #paintViewport, which painted the surround and the page identically and left
   a 1px hairline as the only thing telling you where the page was. Sitting on
   the canvas it also pans, zooms and (since NAOC-121) rotates with the page,
   the way a sheet of paper does.

   background-size and the edge shadow are overwritten per-frame by
   applyTransform() in paintMain.js, which divides them by the zoom so they
   stay a constant size on screen -- a CSS transform scales an element's
   background and box-shadow along with its pixels, which is why the old 1px
   border rendered at a fifth of a pixel when zoomed out. */
#paintViewport canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: auto;
  height: auto;
  transform-origin: 0 0;
  background:
    linear-gradient(45deg, var(--checker) 25%, transparent 25%, transparent 75%, var(--checker) 75%, var(--checker)),
    linear-gradient(45deg, var(--checker) 25%, transparent 25%, transparent 75%, var(--checker) 75%, var(--checker));
  background-size: 20px 20px;
  background-position: 0 0, 10px 10px;
  background-color: var(--checker-base);
  box-shadow: 0 0 0 1px var(--page-edge);
  image-rendering: pixelated;
}
#viewport.drag-over::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 2px dashed var(--accent);
  pointer-events: none;
}
#hint {
  position: absolute;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  color: var(--muted);
  font-size: 12px;
  background: var(--scrim);
  padding: 6px 12px;
  border-radius: 20px;
  pointer-events: none;
}
/* On-canvas layer overview -- a stack of square thumbnails in screen
   space (not inside #paintViewport, so it doesn't pan/zoom with the
   canvas), letting you see and switch layers without looking at the
   side panel's Layers list. Top-of-stack layer shown at the top,
   matching #layerList's display order. */
/* No panel chrome of its own: the thumbnails carry borders and the object
   rows carry their own background, so a slab behind them was redundant --
   and being a fixed-width block, it covered more of the artwork than the
   contents needed. The container is now invisible and hugs its contents. */
#layerOverview {
  position: absolute;
  bottom: 14px;
  left: 14px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  width: max-content;
}
.layer-overview-thumb {
  width: 44px;
  height: 44px;
  border-radius: 4px;
  border: 1px solid var(--border);
  cursor: pointer;
  display: block;
}
.layer-overview-thumb:hover { border-color: var(--accent); }
.layer-overview-thumb.active { border-color: var(--accent); border-width: 2px; }
/* Hover tips the app draws itself (NAOC-336). A native tooltip's timing
   belongs to the browser, so these exist to arrive in half a second instead of
   one or two -- which matters here because almost every control is icon-only
   and the tip is the only label it has.

   Deliberately close to what a native tooltip looks like: this replaces 86 of
   them, and a reader should not be able to tell which controls were converted
   and which were not. */
.hover-tip {
  position: fixed;
  z-index: 4000;
  max-width: 30ch;
  padding: 5px 8px;
  border-radius: 5px;
  background: var(--tip-bg, #2b2f38);
  color: var(--tip-fg, #f2f3f7);
  border: 1px solid var(--tip-border, #3d424e);
  font-size: 12px;
  line-height: 1.35;
  /* NEVER A HOVER TARGET ITSELF. It is positioned under the pointer, so
     without this it would take the pointerover, the element beneath would read
     as left, and the tip would flicker itself out of existence. */
  pointer-events: none;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
}

#empty-state {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 13px;
  text-align: center;
  pointer-events: none;
  /* WRAPS, because this is not always one short line (NAOC-331). Arriving
     from "Send to 3D Editor" it carries a sentence saying the model is on its
     way and safe in the asset library, and with no width that ran edge to edge
     across the viewport as a single line. */
  padding: 0 24px;
}

/* The long form of this message is a sentence rather than a label, so it is
   capped to a readable measure instead of running the width of the viewport. */
#empty-state .arriving {
  max-width: 46ch;
}


/* Dimension numbers (NAOC-161). These sit ON their dimension line rather than
   in a chip beside it, so almost all the chrome is gone: no border, no radius,
   no badge. Positioned AND rotated every frame in JS to lie along the line
   they belong to -- see updateDimLabels in editorMain.js.

   The line still has to be broken behind the text -- a drawing runs the
   dimension line up to the number, stops, and carries on the other side --
   but a filled background is the wrong way to do it. --scrim-strong is
   rgba(255,255,255,0.92) in the light theme, so a "subtle" scrim came out as
   a solid white chip: exactly the thing this ticket removed, reintroduced by
   a token that only looked subtle in the dark theme it was checked in.

   A text-shadow halo knocks the strokes out around the glyphs instead,
   without drawing a box. Repeated because one shadow at this radius is too
   faint to hide a line under it. */
.dim-label {
  position: absolute;
  top: 0;
  left: 0;
  transform: translate(-50%, -50%);
  display: none;
  color: var(--text);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-shadow:
    0 0 3px var(--bg), 0 0 3px var(--bg), 0 0 3px var(--bg),
    0 0 6px var(--bg), 0 0 6px var(--bg);
  padding: 0 3px;
  white-space: nowrap;
  pointer-events: none;
}
/* Tinted to match its own line, so the number and the line read as one
   annotation rather than as a label that happens to be near one. */
/* Guides are transient and about a RELATIONSHIP rather than about an axis, so
   they get one colour of their own instead of the axis palette -- "this
   snapped" is the message, and colouring it by axis would say something the
   dimension lines already say. */
.dim-label.dim-guide { color: #c74bd6; }
/* Pair distances (NAOC-163) -- a different job from the axis dimensions, so a
   colour of their own rather than borrowing an axis's. */
.dim-label.dim-pair { color: #0d7a6f; }
.dim-label.dim-x { color: var(--axis-x); }
.dim-label.dim-z { color: var(--axis-z); }
.dim-label.dim-y { color: var(--axis-y); }

/* Click-to-edit: only enabled with exactly one object selected (see
   updateDimLabels) -- otherwise there's no single unambiguous target to
   resize/reposition, so the label stays a plain read-only overlay. */
.dim-label.editable {
  pointer-events: auto;
  cursor: text;
}
/* No border left to thicken, so the hover cue is the text itself. */
.dim-label.editable:hover { text-decoration: underline; }
.dim-edit-input {
  width: 52px;
  background: transparent;
  color: var(--text);
  border: none;
  outline: none;
  font: inherit;
  font-variant-numeric: tabular-nums;
  padding: 0;
}

/* Live move/rotate/scale readout, shown only while actively dragging the
   gizmo (see updateDragHud in editorMain.js). */
.drag-hud {
  position: absolute;
  top: 0;
  left: 0;
  transform: translate(-50%, calc(-100% - 14px));
  display: none;
  color: white;
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, var(--accent) 92%, transparent);
  padding: 5px 10px;
  border-radius: 6px;
  white-space: nowrap;
  pointer-events: none;
}

/* Read-only text laid straight over the canvas, top-right -- no card, no
 * border. What's selected is feedback about the thing you're already looking
 * at, so it should be readable without a glance away to the sidebar, and it
 * shouldn't announce itself as a panel. `pointer-events: none` matters: this
 * sits over the viewport, and orbiting by dragging across it has to keep
 * working. No text-shadow: it reads as blur at this size rather than as
 * contrast. */
.canvas-readout {
  position: absolute;
  top: 12px;
  right: 12px;
  text-align: right;
  pointer-events: none;
  color: var(--text);
  font-size: 12px;
  line-height: 1.6;
  max-width: 40%;
}
.canvas-readout #selectionLabel { font-weight: 600; }
.canvas-readout #selectionDims { color: var(--muted); font-variant-numeric: tabular-nums; }

/* Sub-object granularity switch (NAOC-20). Bottom-centre: it belongs to the
 * SELECTION, so it sits over the canvas near the hint line rather than in a
 * panel you have to look away to reach -- and centred, so it doesn't collide
 * with the workspace-settings button bottom-left or the collection strip on
 * the right. Hidden entirely with nothing selected, because a granularity for
 * a selection that doesn't exist is a control that can only confuse. */
.submode-bar {
  position: absolute;
  left: 50%;
  bottom: 56px;
  transform: translateX(-50%);
  display: flex;
  gap: 2px;
  padding: 2px;
  background: var(--scrim);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.submode-bar button {
  padding: 4px 12px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}
.submode-bar button:hover:not(:disabled) { color: var(--text); }
.submode-bar button.active {
  color: var(--text);
  background: var(--panel);
  font-weight: 600;
}
/* Not yet built, rather than not applicable -- shown greyed so the shape of
 * the feature is visible instead of appearing later without warning. */
.submode-bar button:disabled { opacity: 0.38; cursor: default; }

/* Corner control on the canvas itself (bottom-left: workspace settings).
 * Tinkercad puts its grid/snap control in exactly this corner, and the
 * placement carries meaning -- these are settings for the space you're
 * building in, not for the document or the selection. */
.canvas-corner-btn {
  position: absolute;
  left: 12px;
  bottom: 12px;
  width: 32px;
  height: 32px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}
.canvas-corner-btn:hover { border-color: var(--accent); color: var(--text); }
.canvas-corner-btn.active { background: var(--accent); border-color: var(--accent); color: white; }
.canvas-corner-btn svg { width: 18px; height: 18px; display: block; }

/* A titled group of settings inside a panel. The rule is the separator --
 * two stacks of checkboxes and number fields butted together read as one
 * undifferentiated list otherwise. */
.settings-group {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.settings-group-label {
  color: var(--muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 8px;
}

/* Outliner: the scene tree. Rows are flat with an indent, so depth reads
 * without nested containers. */
#outliner {
  max-height: 260px;
  overflow-y: auto;
}
.tree-row {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px 4px 3px 6px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  color: var(--text);
}
.tree-row:hover { background: var(--hover); }
.tree-row.selected { background: var(--accent); color: white; }
/* Hidden and locked rows read as unavailable but stay legible -- you still
 * have to be able to find the thing in order to unhide it. */
.tree-row.is-hidden .tree-name { opacity: 0.45; font-style: italic; }
.tree-row.is-locked .tree-name { opacity: 0.6; }
.tree-row.is-hidden, .tree-row.is-locked { cursor: default; }
.tree-twisty {
  width: 14px;
  flex: 0 0 14px;
  padding: 0;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 9px;
  line-height: 1;
  cursor: pointer;
}
.tree-twisty-empty { cursor: default; }
.tree-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 1px 2px;
}
.tree-rename {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12px;
  padding: 1px 3px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--accent);
  border-radius: 3px;
}
.tree-toggle {
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: 3px;
  /* Off-state toggles are nearly invisible until you hover the row: three
   * lit icons on every row would be visual noise on a long tree, and their
   * only job when off is to be findable. */
  color: var(--muted);
  opacity: 0.35;
  cursor: pointer;
}
.tree-row:hover .tree-toggle { opacity: 0.8; }
.tree-toggle:hover { background: var(--hover); opacity: 1; }
.tree-toggle.active { opacity: 1; color: var(--accent); }
.tree-row.selected .tree-toggle { color: white; }
.tree-toggle.partial { opacity: 0.7; }
.tree-toggle svg { width: 14px; height: 14px; display: block; }

/* A shape a boolean kernel will refuse (NAOC-143). Deliberately NOT dimmed
 * like the toggles beside it: those are controls whose off-state should stay
 * quiet, this is a finding, and it stays lit without hovering because the
 * whole point is that it used to be invisible until some later operation
 * failed. Warning amber rather than the error red, since the object is still
 * perfectly usable for everything except booleans. */
.tree-unsound {
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--warn);
  cursor: help;
}
.tree-unsound svg { width: 14px; height: 14px; display: block; }
.tree-row.selected .tree-unsound { color: #ffd9a0; }

/* ---------- collections, on the canvas (NAOC-114) ----------
   A vertical strip down the left edge. Vertically centred rather than pinned
   to a corner, because both left corners are already spoken for -- the Mode
   panel docks top-left and the canvas-settings button sits bottom-left -- and
   centring stays clear of them however many collections there are. */
#collectionOverview {
  position: absolute;
  /* RIGHT edge, not left: the tool palette now sits against the canvas's left
     side (NAOC-116 pass 3), and a strip there would read as a second column
     of it. The right edge carries only the selection readout, which is text
     at the very top. */
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  max-height: 76%;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 2px 4px 2px 2px;
}
/* No panel chrome: the tiles carry their own borders, so a slab behind them
   would only cover more of the model than the contents need -- the same
   reasoning the 2D layer strip settled on. */
.collection-add {
  width: 22px;
  height: 22px;
  padding: 0;
  line-height: 1;
  font-size: 14px;
  color: var(--text);
  background: var(--scrim);
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  flex: none;
}
.collection-add:hover { border-color: var(--accent); }
.collection-tile {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  width: 54px;
  flex: none;
  cursor: pointer;
}
/* The name goes ABOVE the thumbnail, not beside it: at 44px a name alongside
   would either clip to uselessness or make the strip as wide as the sidebar
   this replaced. */
.collection-tile-name {
  font-size: 9px;
  line-height: 1.2;
  color: var(--text);
  text-shadow: 0 1px 2px var(--on-canvas-shadow);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border-bottom: 2px solid transparent;   /* carries the colour override */
}
.collection-tile-thumb {
  width: 44px;
  height: 44px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--scrim);
  display: block;
}
.collection-tile:hover .collection-tile-thumb { border-color: var(--accent); }
.collection-tile.current .collection-tile-thumb { border-color: var(--accent); border-width: 2px; }
/* Empty has to look deliberate rather than broken -- a dashed frame says
   "nothing in here yet" where a blank tile says "the render failed". */
.collection-tile-thumb.is-empty {
  border-style: dashed;
  color: var(--muted);
  font-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.collection-tile.is-hidden .collection-tile-name,
.collection-tile.is-hidden .collection-tile-thumb { opacity: 0.45; }
.collection-tile.is-soloed .collection-tile-name { color: var(--warn); }
.collection-tile-toggles {
  display: flex;
  gap: 1px;
  justify-content: center;
  width: 44px;
}
.collection-tile-toggles .tree-toggle {
  padding: 1px 2px;
  background: var(--scrim);
  border-radius: 3px;
}
.collection-tile-toggles .tree-toggle svg { width: 11px; height: 11px; }
.tree-toggle.solo.active { color: var(--warn); }

/* Drop feedback. "Into" fills the tile, before/after draw a line at its edge
   -- the two mean different things (join it vs reorder past it) and have to
   look different, or there is no way to tell which you are about to get. */
.collection-tile[data-drop="into"] .collection-tile-thumb {
  border-color: var(--accent);
  border-width: 2px;
  background: color-mix(in srgb, var(--accent) 35%, transparent);
}
.collection-tile[data-drop="before"] { box-shadow: 0 -3px 0 -1px var(--accent); }
.collection-tile[data-drop="after"] { box-shadow: 0 3px 0 -1px var(--accent); }
#collectionOverview.drop-root { box-shadow: inset 0 0 0 1px var(--accent); border-radius: 4px; }

/* The collection's own settings, inside the floating panel. `width: auto`
   because button.action is width: 100% by design (it is normally the only
   thing on its row); here "None" sits beside a label and a swatch, and full
   width would make the escape hatch read as the primary control. */
#collectionPanel #collectionColorClear {
  width: auto;
  flex: 0 0 auto;
  padding: 4px 8px;
  font-size: 11px;
}
#collectionPanel #collectionColorClear:disabled { opacity: 0.4; cursor: default; }

/* Editor-specific additions (asset library list, etc.) */
#assetList, #projectList, #documentList, #layerList {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 220px;
  overflow-y: auto;
}
.asset-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
}
.asset-drag-handle {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
  cursor: grab;
}
.asset-row .asset-thumb {
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  border-radius: 4px;
  background: var(--field);
  border: 1px solid var(--border);
}
.asset-row .asset-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asset-row button {
  background: var(--control);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 4px 8px;
  font-size: 11px;
  cursor: pointer;
  flex: 0 0 auto;
}
.asset-row button:hover { border-color: var(--accent); }
.layer-row.active { outline: 1px solid var(--accent); border-radius: 4px; }
.layer-row { flex-wrap: wrap; row-gap: 4px; padding: 4px; }
.layer-row input[type="range"] { width: 48px; }
.layer-row .layer-visible { flex: 0 0 auto; }
.layer-row .asset-name { flex: 1 1 auto; min-width: 60px; }
.layer-row select { flex: 0 0 auto; max-width: 84px; font-size: 11px; }
.asset-empty {
  color: var(--muted);
  font-size: 12px;
}
.asset-category-label {
  color: var(--muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 10px 0 2px;
}
.asset-category-label:first-child { margin-top: 0; }
/* Only a real category is renameable; "Uncategorized" is the absence of one,
 * so it deliberately gets no affordance. */
.asset-category-label.is-renameable { cursor: text; }
.asset-category-label.is-renameable:hover { color: var(--text); }

/* ---------- contextual panels (src/contextPanel.js) ----------
   Controls that belong to the current tool or selection float over the
   canvas instead of living in the left column, so showing one no longer
   pushes the rest of the sidebar around. Positioned by JS; everything here
   is appearance plus the constraints that keep them usable over artwork. */
.context-panel {
  position: absolute;
  z-index: 20;
  width: 250px;
  max-height: calc(100% - 24px);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  /* Reads as floating above the canvas rather than as part of it. */
  box-shadow: 0 8px 24px var(--shadow);
}
.context-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: move;
  user-select: none;
}
.context-panel-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
}
.context-panel-close:hover { color: var(--text); }
.context-panel-body {
  padding: 10px;
  overflow-y: auto;
  /* WITHOUT THIS THE overflow-y ABOVE NEVER ENGAGES (NAOC-328). A flex item
     defaults to min-height:auto and refuses to shrink below its content, so
     the body grew past the panel's own frame and the declared scroll could
     never happen. It went unnoticed while every context panel was shorter
     than the window; the canvas settings panel became the first one that is
     not, once the workspace settings moved into it.

     MEASURED at 1024x600, where it matters: the body is clipped to the panel
     at 587 with scrollHeight 601 against clientHeight 493, so the overflow
     scrolls instead of running on. At 1400x900 the content fits and nothing
     scrolls, which is why this changes nothing at the size everything is
     usually looked at. */
  min-height: 0;
}
/* Panels adopt fieldsets that were styled for the sidebar; drop the second
   border and the legend now that the panel header names the thing. */
.context-panel-body fieldset {
  border: none;
  margin: 0;
  padding: 0;
}
.context-panel-body fieldset + fieldset { margin-top: 12px; }
.context-panel-body legend { display: none; }

/* The brush/eraser/liquify reach ring (NAOC-97). Two stacked strokes -- a
   light border with a dark shadow just outside it -- so the ring stays
   readable over both dark and light artwork without tinting what is under
   it. `pointer-events: none` matters: the ring sits directly under the
   cursor, so anything else would have it swallow every click it describes. */
.tool-ring {
  position: absolute;
  border: 1px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
  pointer-events: none;
  z-index: 3;
}



/* A context menu reuses .menu-popover's look, but not its geometry: that rule
   stretches left:0/right:0 to fill a .menu-button wrapper it does not have
   here, so without this it spans the whole viewport. */
.menu-popover.context-menu {
  right: auto;
  top: auto;
  width: max-content;
  min-width: 160px;
  max-width: 240px;
}

/* ---------- floating layer strip (NAOC-103) ----------
   The sidebar's Layers panel is gone; this strip over the canvas is the
   whole layer UI now. Sized so the column stays out of the way at the
   bottom-left corner rather than becoming a second sidebar. */
.layer-overview-item {
  position: relative;
  line-height: 0;
  border-radius: 4px;
}
.layer-overview-item.active .layer-overview-thumb {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}
/* A hidden layer still needs to be clickable to unhide it, so it is dimmed
   rather than removed -- and dimming reads at 44px where the small toggle
   in the corner does not. */
.layer-overview-item.hidden-layer .layer-overview-thumb { opacity: 0.35; }
.layer-overview-eye {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 13px;
  height: 13px;
  margin: 0;
  cursor: pointer;
  opacity: 0.85;
}
.layer-overview-add {
  width: 44px;
  height: 22px;
  border: 1px dashed var(--border);
  border-radius: 4px;
  background: none;
  color: var(--on-canvas);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.layer-overview-add:hover {
  color: var(--text-strong);
  border-color: var(--accent);
}

/* Close button on popover menus. Sits in its own row at the top so it never
   overlaps an item's text, and is right-aligned where a close control is
   expected. */
.menu-popover-close-row {
  display: flex;
  justify-content: flex-end;
  padding: 0 2px 2px;
  margin: 0;
}
.menu-popover .menu-popover-close {
  width: 20px;
  height: 20px;
  padding: 0;
  line-height: 1;
  font-size: 15px;
  color: var(--muted);
  background: transparent;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  text-align: center;
}
.menu-popover .menu-popover-close:hover {
  color: var(--text);
  background: var(--hover);
}

/* A .row is a vertical stack (label above control), so a bare checkbox added
   as the row's first child ends up floating on its own line above the label
   it belongs to. These rows are "toggle + label + control" and want to read
   left-to-right. */
.row.toggle-row {
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
.row.toggle-row > label { flex: 1; align-items: center; }
.row.toggle-row > input[type="checkbox"] { margin: 0; flex: none; }

/* ---------- primary / secondary colour pair (Photoshop-style) ---------- */
.color-pair {
  display: flex;
  align-items: center;
  gap: 8px;
}
.color-slot { position: relative; line-height: 0; }
.color-slot input[type="color"] {
  width: 46px;
  height: 46px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  background: none;
}
/* The 1/2 badge. Two colour wells side by side are ambiguous about which is
   which, and every tool's behaviour now depends on knowing. */
.color-slot-badge {
  position: absolute;
  right: -3px;
  bottom: -3px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 8px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 10px;
  line-height: 14px;
  text-align: center;
}
.color-swap { width: auto; padding: 4px 8px; font-size: 14px; }

/* ---------- object tree inside the layer strip ---------- */
/* Collapsed, the strip stays a narrow column of thumbnails. Expanded, it
   widens to fit object rows and scrolls, so a layer with 50 objects does not
   run off the top of the screen. */
#layerOverview {
  max-height: 70vh;
  overflow-y: auto;
  overflow-x: hidden;
}
/* Expanded, it grows to fit the longest object name rather than to a fixed
   width, and stops before it becomes a second sidebar. */
#layerOverview.has-tree { max-width: 190px; }
.layer-overview-twisty {
  position: absolute;
  right: 1px;
  bottom: 1px;
  width: 15px;
  height: 15px;
  padding: 0;
  line-height: 1;
  font-size: 10px;
  color: var(--text-strong);
  background: var(--scrim);
  border: none;
  border-radius: 3px;
  cursor: pointer;
}
.layer-overview-twisty:hover { background: var(--accent); }
.layer-objects {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 2px 0 2px 10px;   /* indented under its layer */
}
.layer-object-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 5px;
  border: 1px solid transparent;
  border-radius: 4px;
  background: var(--scrim);
  cursor: grab;
  font-size: 11px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
}
.layer-object-row.active { border-color: var(--accent); background: var(--accent-tint); }
.layer-object-row:hover { background: var(--hover); }
.layer-object-chip {
  flex: none;
  width: 11px;
  height: 11px;
  border-radius: 2px;
  border: 1px solid var(--border);
}
.layer-object-name { overflow: hidden; text-overflow: ellipsis; }
/* While a reorder is in flight, suppress the hover highlight -- the row under
   the cursor is not the one you are dragging. */
#layerOverview.is-reordering .layer-object-row:hover,
.layer-objects.is-reordering .layer-object-row:hover { background: var(--scrim); }
#layerOverview.is-reordering, .layer-objects.is-reordering { cursor: grabbing; }

/* The strip lives inside #viewport, so `#viewport canvas { width:100% }`
   (which stretches the 3D/paint canvas to fill) outranks the class rule and
   was scaling layer thumbnails to the full strip width. Only visible once
   the strip widened for the object tree. */
#viewport .layer-overview-thumb {
  width: 44px;
  height: 44px;
  flex: none;
}
#layerOverview .layer-overview-item { width: 46px; }

/* The layer tile you are about to drop an object onto. */
.layer-overview-item.drop-target-hot {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* ---------------------------------------------------------------------------
   The in-app walkthrough (NAOC-196).

   Bottom-left, clear of the canvas hint strip along the bottom-centre and of
   the tool panel on the left edge. It has to sit ABOVE the canvas without
   taking its clicks: the reader is being asked to operate the app while
   reading, so anything the panel swallows is a step they cannot complete.
   --------------------------------------------------------------------------- */
.walkthrough {
  position: fixed;
  /* `left` is set from the canvas's own rect when the panel renders, not
     guessed from a variable -- the tool panel's width is not a constant and a
     wrong guess puts this over the sidebar or out in the middle. */
  bottom: 76px;   /* clear of the canvas hint strip along the bottom */
  width: 330px;
  max-width: calc(100vw - 36px);
  background: var(--panel-bg, #fff);
  color: var(--text, #1c1e21);
  border: 1px solid var(--border, #dfe1e5);
  border-radius: 12px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.16);
  padding: 14px 16px 12px;
  z-index: 60;
  font-size: 13px;
  line-height: 1.5;
}
.walkthrough-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.walkthrough-progress {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted, #85898f);
  font-variant-numeric: tabular-nums;
}
.walkthrough-title { font-weight: 600; font-size: 14px; margin: 0 0 4px; }
/* What a tutorial starts from, said once on its first step. Quieter than the
   prose: it is context rather than an instruction, and a reader who is already
   looking at the scene should not have to read past it. */
.walkthrough-scene {
  margin: 0 0 8px; font-size: 12px; line-height: 1.45;
  color: var(--muted, #85898f);
}
.walkthrough-prose { margin: 0 0 10px; color: var(--text-dim, #33363a); }
/* Items keep their own text on one line -- left to flex, "Read this step"
   collapses into a two-character column -- but the ROW is allowed to wrap, so
   adding an item can never push the last one out through the panel's edge.
   That is exactly how Quit ended up half outside the box. */
.walkthrough-foot {
  display: flex; align-items: center; gap: 8px 10px; flex-wrap: wrap;
}
.walkthrough-foot > * { white-space: nowrap; }
.walkthrough-foot a {
  color: var(--accent, #2f5fe0); text-decoration: none; font-size: 12px; flex: 0 0 auto;
}
.walkthrough-foot a:hover { text-decoration: underline; }
/* button.action carries width:100% for the sidebar's stacked buttons, and it
   out-specifies a bare class -- so this needs the element selector too, or
   Quit stretches past the panel's own edge. */
.walkthrough button.walkthrough-quit {
  margin-left: auto;
  flex: 0 0 auto;
  width: auto;
  padding: 5px 14px;
}
/* Waiting is the normal state, so it is quiet -- a spinner would suggest the
   app is busy when it is the reader who is being waited for. Its own line,
   above the buttons; see renderWalkthroughPanel for why. */
.walkthrough-waiting {
  font-size: 11px; color: var(--muted, #85898f); margin-bottom: 8px;
}
.walkthrough-done .walkthrough-title { color: var(--accent, #2f5fe0); }

/* The coach mark: a ring on the control the current step is about (NAOC-197).
   Never takes a click -- the reader is being asked to press the thing it is
   drawing attention to. */
.coach-mark {
  position: fixed;
  border: 3px solid var(--accent, #2f5fe0);
  border-radius: 10px;
  box-shadow: 0 0 0 3px rgba(47, 95, 224, 0.22);
  pointer-events: none;
  z-index: 55;
  transition: left 90ms linear, top 90ms linear, width 90ms linear, height 90ms linear;
}
.coach-mark-round { border-radius: 50%; }
.walkthrough button.walkthrough-show {
  flex: 0 0 auto;
  width: auto;
  padding: 5px 12px;
}
/* Quit is the only thing pushed right, and it is the only thing that should
   be: it is the way out, not one of the actions. When the status line lived in
   this row, Show me carried the auto margin instead and Quit sat beside it --
   with the status line moved above, that left the whole row floating to the
   right against nothing. */

/* ---------------------------------------------------------------------------
   The Tutorials browser (NAOC-194).

   Doubles as a help layer: while it is open, every control some tutorial
   teaches gets a dot, and clicking one asks "what teaches this?" instead of
   doing its normal job. So the panel has to sit above the app without
   covering the tool palette the reader is being invited to click.
   --------------------------------------------------------------------------- */
.tutorials-browser {
  position: fixed;
  top: 58px;
  right: 18px;
  width: 340px;
  max-height: calc(100vh - 96px);
  display: flex;
  flex-direction: column;
  background: var(--panel-bg, #fff);
  color: var(--text, #1c1e21);
  border: 1px solid var(--border, #dfe1e5);
  border-radius: 12px;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.18);
  z-index: 70;
  font-size: 13px;
}
.tutorials-head {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 14px 8px;
}
.tutorials-head strong { font-size: 14px; flex: 1 1 auto; }
.tutorials-browser input[type="search"] {
  margin: 0 14px 8px;
  padding: 6px 9px;
  border: 1px solid var(--border, #dfe1e5);
  border-radius: 7px;
  background: var(--control, #fff);
  color: inherit;
  font: inherit;
}
.tutorials-list { overflow-y: auto; padding: 0 14px 12px; }
.tutorials-category {
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted, #85898f); margin: 10px 0 6px;
}
.tutorial-row {
  border: 1px solid var(--border, #dfe1e5);
  border-radius: 9px; padding: 9px 10px; margin-bottom: 8px;
}
.tutorial-row h4 { margin: 0 0 3px; font-size: 13px; }
.tutorial-row p { margin: 0 0 8px; color: var(--muted, #85898f); font-size: 12px; line-height: 1.45; }
.tutorial-row .row-actions { display: flex; gap: 8px; align-items: center; }
/* button.action is width:100% for the sidebar's stacked buttons and
   out-specifies a bare class, so every button inside a floating panel needs
   the element selector to escape it. This is the second panel to trip on it. */
.tutorials-browser button.action { width: auto; flex: 0 0 auto; padding: 4px 11px; }
.tutorial-row .row-actions a { font-size: 12px; color: var(--accent, #2f5fe0); text-decoration: none; }
.tutorial-row .row-actions a:hover { text-decoration: underline; }
.tutorials-empty { color: var(--muted, #85898f); padding: 6px 0 2px; }
/* Why a tutorial would not start, said where the reader pressed rather than
   in a corner of the window they are not looking at. */
.tutorial-row .row-refused {
  margin: 8px 0 0; font-size: 12px; line-height: 1.45;
  color: var(--warn, #a97706);
}
.tutorials-hint {
  font-size: 11px; color: var(--muted, #85898f);
  border-top: 1px solid var(--border, #dfe1e5);
  padding: 8px 14px; line-height: 1.45;
}
/* A control some tutorial teaches, while the browser is open. Small and in
   the corner: this is a hint layered over a working app, not a takeover. */
.has-tutorial {
  position: fixed;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--accent, #2f5fe0);
  box-shadow: 0 0 0 2px var(--panel-bg, #fff);
  pointer-events: none;
  z-index: 65;
}

/* ---------------------------------------------------------------------------
 * Declared panels (NAOC-202).
 *
 * The panel runtime builds these from a tool's declaration, so it cannot know
 * what any particular control should look like -- it stamps the control KIND
 * on the row and the rules below do the rest. That keeps style in the
 * stylesheet and out of the contract.
 *
 * These exist because the first declared panels looked foreign beside the
 * hand-written ones and in two places were plainly broken: the mm unit
 * orphaned on its own line under the slider, and checkboxes floating at the
 * far right of their labels. The suite was green throughout -- a test cannot
 * see a layout.
 * ------------------------------------------------------------------------ */
.declared-panel { display: flex; flex-direction: column; gap: 10px; }
.declared-row { margin-bottom: 0; }

/* A number and its optional slider and unit read as ONE control, on one line:
 * [ 3 ] [======slider======] mm */
.declared-number {
  display: flex;
  align-items: center;
  gap: 8px;
}
.declared-number input[type="range"] { flex: 1; min-width: 0; }
.declared-unit { font-size: 12px; color: var(--muted); }

/* A checkbox belongs beside its label, not below it and hard right -- which is
 * what a column row plus a stretched label produced. */
.declared-row[data-control="checkbox"] {
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
.declared-row[data-control="checkbox"] .declared-label { order: 2; flex: 1; }
.declared-row[data-control="checkbox"] input[type="checkbox"] {
  order: 1;
  accent-color: var(--accent);
}
/* Disabled has to LOOK disabled: Bold greys out for a font with no bold cut,
 * and a reader who cannot see that reads it as a control that does nothing. */
.declared-row input:disabled,
.declared-row select:disabled { opacity: 0.45; cursor: not-allowed; }
.declared-row:has(input:disabled) .declared-label { color: var(--muted); }

.declared-row .declared-label { font-size: 12px; color: var(--text); }
/* A DECLARED segmented choice wraps; .mode-toggle itself does not (NAOC-215).
 * Scoped to this class deliberately: editor.html and index.html reuse
 * .mode-toggle for their own full-width text buttons, and the fix is about the
 * control type, not about every use of that look. A grid rather than a wrapping
 * flex row because flex items with basis 0 never shrink below their text, which
 * is what put Reorient's sixth plane outside the panel in the first place. The
 * column count comes from the control type, which is the only thing that knows
 * how many options there are; see choiceColumns. */
.declared-choice {
  margin-top: 2px;
  display: grid;
  grid-template-columns: repeat(var(--choice-cols, 3), 1fr);
}


/* ---------- the landing page (NAOC-287) ----------
   Its own small block rather than reusing the editors' chrome: this page has
   no document and no tools, so it shares the palette and the type and nothing
   else. */
body.landing {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
}
.landing-main {
  max-width: 54rem;
  margin: 0 auto;
  padding: 4rem 1.5rem 5rem;
}
.landing-main h1 {
  font-size: 2.2rem;
  margin: 0 0 .6rem;
  letter-spacing: -0.02em;
}
.landing-lede {
  font-size: 1.05rem;
  line-height: 1.6;
  color: var(--muted);
  margin: 0 0 2.5rem;
  max-width: 42rem;
}
.landing-tools {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
}
.landing-card {
  display: block;
  padding: 1.1rem 1.2rem 1.3rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel-bg, var(--bg));
  color: inherit;
  text-decoration: none;
  transition: border-color .15s, transform .1s;
}
.landing-card:hover { border-color: var(--accent); }
.landing-card:active { transform: translateY(1px); }
.landing-card h2 { font-size: 1.05rem; margin: 0 0 .35rem; }
.landing-card p { margin: 0; font-size: .9rem; line-height: 1.5; color: var(--muted); }
.landing-note {
  margin: 2.5rem 0 0;
  font-size: .9rem;
  color: var(--muted);
}

/* The admin page (NAOC-356). Plain on purpose: a table, not a panel. */
.admin-main { max-width: 1100px; margin: 0 auto; padding: 24px 16px; }
.admin-lede { color: var(--muted); margin: 4px 0 18px; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.admin-table th, .admin-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--control); vertical-align: top; }
.admin-table th { color: var(--muted); font-weight: 600; }
.admin-note { color: var(--muted); font-size: 12px; }
.admin-spent { color: #c0392b; }
.topbar-admin[hidden] { display: none; }

/* ---- landing page, second pass (NAOC-287) ------------------------------ */
.landing-hero { max-width: 720px; margin: 0 auto 3rem; text-align: left; }
.landing-brand { margin: 0 0 .5rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; font-size: .8rem; color: var(--accent); }
.landing-main h1 { font-size: clamp(1.9rem, 4.5vw, 2.8rem); line-height: 1.15; margin: 0 0 1rem; }
.landing-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1.4rem; }
.landing-cta { display: inline-block; padding: .65rem 1.1rem; border-radius: 8px; background: var(--accent); color: #fff; font-weight: 600; text-decoration: none; }
.landing-cta-quiet { background: var(--control); color: var(--text); }
.landing-cta:hover { filter: brightness(1.08); }
/* The dark theme's accent is a lighter blue, and white on it measured 3.16:1
   -- fine for a 1px border, failing for the one button the page is about.
   A deeper fill for the filled button only; links and borders keep --accent. */
:root[data-theme="dark"] .landing-cta:not(.landing-cta-quiet) { background: #3457d5; }
.landing-tools { display: grid; grid-template-columns: 1fr auto 1fr auto 1fr; align-items: stretch; gap: .75rem; max-width: 1000px; margin: 0 auto 3rem; }
.landing-arrow { align-self: center; color: var(--muted); font-size: 1.4rem; }
/* --text, not --muted, on the tinted badge: muted-on-control measured 4.42:1,
   under AA, exactly as it did on the local-work notice (NAOC-359). */
.landing-step { display: inline-block; width: 1.6rem; height: 1.6rem; line-height: 1.6rem; text-align: center; border-radius: 50%; background: var(--control); color: var(--text); font-size: .8rem; font-weight: 600; margin-bottom: .6rem; }
.landing-plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; max-width: 720px; margin: 0 auto 2.5rem; }
.landing-plan { border: 1px solid var(--control); border-radius: 10px; padding: 1.1rem 1.2rem; background: var(--panel, transparent); }
.landing-plan-maker { border-color: var(--accent); }
.landing-plan h3 { margin: 0 0 .25rem; font-size: 1rem; }
.landing-price { margin: 0 0 .75rem; font-size: 1.6rem; font-weight: 700; }
.landing-price span { font-size: .85rem; font-weight: 400; color: var(--muted); margin-left: .3rem; }
.landing-plan ul { margin: 0; padding-left: 1.1rem; color: var(--muted); font-size: .92rem; line-height: 1.6; }
.landing-trial { margin: .8rem 0 0; font-size: .9rem; color: var(--text); }

/* The legal pages (NAOC-361) and the footer that reaches them. A reading
   column rather than the landing page's grid: these are read top to bottom,
   and a line much past 70 characters is where a reader loses their place. */
.legal-main { max-width: 680px; margin: 0 auto; padding: 2.5rem 1.25rem 2rem; line-height: 1.6; }
.legal-main h1 { font-size: clamp(1.7rem, 4vw, 2.2rem); margin: 0 0 .25rem; }
.legal-main h2 { font-size: 1.15rem; margin: 2rem 0 .5rem; }
.legal-main h3 { font-size: 1rem; margin: 1.25rem 0 .35rem; }
.legal-main p, .legal-main li { font-size: .95rem; }
.legal-main ul { padding-left: 1.2rem; }
.legal-main a { color: var(--accent); }
.legal-main .landing-brand a { color: inherit; text-decoration: none; }
.legal-effective { margin: 0 0 1.5rem; color: var(--muted); font-size: .9rem; }
.legal-summary { border: 1px solid var(--control); border-radius: 10px; padding: .25rem 1.2rem .5rem; margin: 1.5rem 0; }
.legal-summary h2 { margin-top: .9rem; }
.landing-footer { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem 1.5rem; padding: 1.5rem 1rem 2.5rem; font-size: .85rem; }
.landing-footer a { color: var(--muted); }
.landing-footer a:hover, .landing-footer a[aria-current="page"] { color: var(--text); }
@media (max-width: 760px) {
  .landing-tools { grid-template-columns: 1fr; }
  .landing-arrow { transform: rotate(90deg); justify-self: center; }
}
