/* App-level extensions on top of the ported design system (components/base.css).
 * Everything here is 3Drawers-Rails-specific: element defaults, native <dialog>
 * adjustments, Rails flash messages, and the dimension fieldset that wraps the
 * design system's .ds-stepper. Uses design-system tokens only. */

/* Every transition/animation in the app (base.css's button/input/tab/etc.
 * hovers included) is built on these three duration tokens — collapsing
 * them here is a single, app-wide reduced-motion switch, without having to
 * hand-edit the verbatim design-system files that declare the individual
 * transitions. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast: 0.01ms;
    --duration-base: 0.01ms;
    --duration-slow: 0.01ms;
  }
}

/* display:flex + min-block-size:100dvh here (and flex:1 on main below) let
   any page opt into filling the viewport by putting flex:1 on its own
   top-level element (see .drawer-builder-shell) — pages that don't opt in
   render exactly as before: a flex column with a single non-growing child
   is laid out identically to normal block flow, just with page background
   (already full-bleed) showing through any leftover space. */
html {
  block-size: 100%;
  color-scheme: light;
}

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

/* base.css never sets line-height on .ds-btn, so it falls back to
   whatever the element's own default resolves to — 1.5 (inherited from
   body) for an <a>-rendered button (ds_button with href:), but the
   browser's own "normal" for a real <button> (used by button_to and
   plain <button> tags), which is a noticeably shorter line box at the
   same font-size. Same padding, same classes, visibly different height —
   e.g. drawers/show.html.erb's Edit (an <a>) next to Export/Delete
   (<button>s). Pinning line-height directly makes padding the only thing
   that drives a button's height, regardless of which tag rendered it. */
.ds-btn {
  line-height: 1;
  /* base.css gives filled variants (primary/secondary/danger) border:none
     and only .ds-btn-ghost an actual border — so a filled button sitting
     next to a ghost button in the same row comes out border-width
     shorter. A transparent border of the same width reserves the same
     space either way; .ds-btn-ghost's own border-color (higher
     specificity, .ds-btn.ds-btn-ghost) still wins and paints over it. */
  border: var(--border-width) solid transparent;
}

body {
  display: flex;
  flex-direction: column;
  min-block-size: 100dvh;
  margin: 0;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
}

/* Pages that opt into locking to exactly one viewport (currently just the
   drawer builder, via .drawer-builder-shell) get a hard-capped body so its
   main can scroll internally instead of the document scrolling. Scoped
   with :has() rather than applied unconditionally — a fixed-height flex
   column forces flex-shrink on every child once content exceeds that
   height, which can silently collapse large children that have no text
   forcing a min-content floor (this shipped as a real regression: the
   marketing page's hero band, sized only by padding around centered text,
   got shrunk to 0 height and clipped by its own overflow:hidden). Bounding
   only pages that actually ask for it keeps every other page's body at
   its original min-block-size-only, grow-with-content behavior, where
   flex-shrink never triggers. */
body:has(.drawer-builder-shell) {
  block-size: 100dvh;
  overflow: hidden;
}

/* --color-secondary/--color-danger at their design-system values (teal-500,
   cherry-500, and dark theme's teal-300) fall short of WCAG AA 4.5:1 for
   the white button-label text painted on top of them (as low as 1.84:1 for
   dark-mode secondary) — and --color-danger doubles as error-hint text
   color, which fails the same way against the page background. teal-600/
   cherry-600 clear 4.5:1 in both roles.
   This can't go directly in tokens/colors.css (verbatim design-system
   copy, see CLAUDE.md) or on :root (tokens/colors.css loads after this
   file — see the <link> order in the layout — so a :root rule here would
   lose the specificity tie to colors.css's own :root block). Declaring it
   on body instead sidesteps that: body is a descendant of the :root
   element, so its own value simply wins by inheritance for everything
   inside it, regardless of load order.
   --color-primary (terracotta) has the same problem — 3.12:1 at its
   current 500-step, and none of the design system's other terracotta
   steps clear 4.5:1 without going as dark as terracotta-700, which would
   noticeably change the brand's signature button color. Left alone
   pending an explicit call on that trade-off rather than changed here. */
body {
  --color-secondary: var(--teal-600);
  --color-secondary-hover: var(--teal-700);
  --color-danger: var(--cherry-600);
  /* --text-tertiary (.ds-hint's color — the app's most-used text style,
     every dims line/hint/canvas caption) sits at 4.1-4.3:1 at its default
     neutral-400, short of the 4.5:1 small text needs. neutral-500 already
     passes but is identical to --text-secondary, which would flatten the
     primary/secondary/tertiary hierarchy down to two visible shades — this
     is a custom step between the two that clears 4.5:1 (~5.0:1) while
     staying visibly lighter than secondary. Dark theme's --text-tertiary
     already passes (5.5:1) and is left alone. */
  --text-tertiary: #6e6e7c;
}

[data-theme="dark"] body {
  --color-secondary: var(--teal-600);
  --color-secondary-hover: var(--teal-700);
  --color-danger: var(--cherry-600);
}

/* The app-wide scroll container — normal pages scroll here instead of on
   the document, so opted-in pages (see .drawer-builder-shell) can lock
   their own height to the viewport and manage scrolling internally without
   fighting document-level scroll. */
main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-block-size: 0;
  overflow-y: auto;
  position: relative;
}

h1, h2, h3 {
  font-family: var(--font-display);
  line-height: var(--leading-tight);
}

h1 {
  font-size: var(--text-2xl);
}

h2 {
  font-size: var(--text-lg);
}

h3 {
  font-size: var(--text-md);
}

/* Sub-section labels within a larger panel (placement inspector's "Bin
   color", "Actions") — same visual weight as .ds-label, but a real heading
   so the section structure is available to screen readers instead of
   flattening to plain paragraphs. */
h4 {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  line-height: var(--leading-normal);
  margin: 0 0 var(--space-2);
}

/* Card titles are usually a link (project/drawer/bin name) — the browser's
   default underline reads as unstyled text sitting in a heading. */
.ds-card h2 a,
.ds-card h3 a {
  color: inherit;
  text-decoration: none;
}

.ds-card h2 a:hover,
.ds-card h3 a:hover {
  color: var(--color-primary);
  text-decoration: underline;
}

a {
  color: var(--text-primary);
}

:focus-visible {
  outline: var(--border-width-thick) solid var(--color-info);
  outline-offset: 2px;
}

[data-paper="kraft"] {
  background-color: var(--surface-sunken);
}

/* App header: logo lockup + real nav links + dark toggle + account actions.
   Same lockup treatment as the marketing nav (see below) but namespaced
   app-nav-* since this one is site-wide, not page-specific. */
.app-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-5);
  background: var(--surface-card);
  border-bottom: var(--border-width) solid var(--border-subtle);
}

.app-nav-lockup {
  display: flex;
  align-items: center;
  gap: var(--space-6);
}

.app-nav-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-md);
  text-decoration: none;
}

.app-nav-stackmark {
  position: relative;
  inline-size: 30px;
  block-size: 26px;
  flex: none;
}

.app-nav-stackmark-tile {
  position: absolute;
  inline-size: 16px;
  block-size: 16px;
  border-radius: 6px;
}

.app-nav-stackmark-tile-1 { background: var(--gold-500); top: 8px; left: 8px; transform: rotate(-6deg); }
.app-nav-stackmark-tile-2 { background: var(--teal-500); top: 4px; left: 4px; transform: rotate(4deg); }
.app-nav-stackmark-tile-3 { background: var(--terracotta-500); top: 0; left: 0; transform: rotate(-2deg); }

.app-nav-link {
  font-weight: 600;
  font-size: var(--text-sm);
  text-decoration: none;
  color: var(--text-secondary);
}

.app-nav-link:hover {
  color: var(--text-primary);
}

.app-nav-avatar {
  inline-size: 32px;
  block-size: 32px;
  border-radius: 50%;
  display: block;
}

/* Account menu — a native <details> (see DesignSystemHelper#ds_menu). Not
   yet part of the ported design system (components/base.css), so it lives
   here for now; worth porting back to the design-system project later. */
.ds-menu {
  position: relative;
}

.ds-menu-trigger {
  cursor: pointer;
  list-style: none;
}

.ds-menu-trigger::-webkit-details-marker {
  display: none;
}

.ds-menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-inline-size: 180px;
  padding: var(--space-2);
  background: var(--surface-card);
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.ds-menu-panel a,
.ds-menu-panel button,
.ds-menu-panel .ds-menu-item {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: none;
  background: none;
  font: inherit;
  text-align: left;
  text-decoration: none;
  color: var(--text-primary);
  border-radius: var(--radius-md);
  cursor: pointer;
}

.ds-menu-panel a:hover,
.ds-menu-panel button:hover {
  background: var(--surface-page);
}

/* Subtle "← Project" back-link (drawer page header) — deliberately not a
   bordered ds-btn-ghost, just a quiet hover target, matching the mock's
   .back-link treatment. */
.drawer-back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-sm);
  padding: var(--space-2);
  border-radius: var(--radius-md);
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}

.drawer-back-link:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

/* Opts the drawer builder page into filling the viewport (see body/main
   above) — the header stays its natural height, .u-split-3 grows to take
   whatever's left so the sidebar/canvas/inspector backgrounds always reach
   the bottom of the window instead of stopping at their content height. */
.drawer-builder-shell {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-block-size: 0;
  overflow: hidden;
}

.drawer-builder-shell .u-split-3 {
  flex: 1;
  min-block-size: 0;
  grid-template-rows: minmax(0, 1fr);
}

/* Drawer builder page header — a full-bleed bar directly under the app-nav,
   matching the mock's .page-header (own background/border, edge to edge),
   not a container-constrained row like other pages' headers. */
.drawer-page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  background: var(--surface-card);
  border-bottom: var(--border-width) solid var(--border-subtle);
}

/* The browser default h1 margin otherwise holds the header at its old
   height regardless of the padding above. */
.drawer-page-header h1 {
  margin: 0;
  font-size: 1.5rem;
}

/* The design system's dialog assumes a JS overlay div; we use the native
 * <dialog> element, so the backdrop comes from ::backdrop instead.
 *
 * Open/close transition on the native <dialog>: `overlay`/`display` need
 * transition-behavior: allow-discrete so the browser defers removing the
 * dialog from the top layer until the opacity/transform transition
 * finishes — otherwise .close() removes it instantly and there's nothing
 * left to animate out. @starting-style supplies the "just before first
 * paint" state for the open transition (dialog#showModal() has no prior
 * frame to transition from otherwise). Browsers without support just get
 * the old instant pop, no fallback needed. */
.ds-dialog {
  opacity: 0;
  transform: translateY(10px) scale(0.97);
  transition: opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out),
    overlay var(--duration-slow) var(--ease-out) allow-discrete,
    display var(--duration-slow) var(--ease-out) allow-discrete;
}

.ds-dialog[open] {
  opacity: 1;
  transform: translateY(0) scale(1);
}

@starting-style {
  .ds-dialog[open] {
    opacity: 0;
    transform: translateY(10px) scale(0.97);
  }
}

.ds-dialog::backdrop {
  background: var(--surface-overlay);
  opacity: 0;
  transition: opacity var(--duration-slow) var(--ease-out),
    overlay var(--duration-slow) var(--ease-out) allow-discrete,
    display var(--duration-slow) var(--ease-out) allow-discrete;
}

.ds-dialog[open]::backdrop {
  opacity: 1;
}

@starting-style {
  .ds-dialog[open]::backdrop {
    opacity: 0;
  }
}

/* Vertical rhythm between stacked form fields (the library's .ds-field only
 * handles its internal gap). */
form .ds-field {
  margin-block-end: var(--space-4);
}

/* Rails flash messages, styled after the design system's toast. Floats over
 * whatever main renders first (e.g. .drawer-page-header) instead of taking
 * its own row in document flow — positioned relative to <main>. Auto-
 * dismisses and can be closed by hand (flash_controller.js); the exit
 * transition lives here so both paths share it. */
.ds-flash-stack {
  position: absolute;
  inset-inline: 0;
  top: var(--space-4);
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  pointer-events: none;
}

.ds-flash {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  background: var(--surface-card);
  border: var(--border-width-thick) solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-body);
  max-width: 42rem;
  pointer-events: auto;
  opacity: 1;
  transform: translateY(0);
  transition: opacity var(--duration-slow) var(--ease-out), transform var(--duration-slow) var(--ease-out);
}

.ds-flash.ds-flash-leaving {
  opacity: 0;
  transform: translateY(-10px);
}

.ds-flash.ds-flash-notice {
  border-color: var(--color-success);
  box-shadow: var(--shadow-lg);
  background: var(--color-success-bg);
  color: var(--color-success-ink);
}

.ds-flash.ds-flash-alert {
  border-color: var(--color-danger);
  box-shadow: var(--shadow-lg);
  background: var(--color-danger-bg);
  color: var(--color-danger-ink);
}

.ds-flash-body {
  margin: 0;
  flex: 1;
}

.ds-flash-close {
  flex: none;
  border: none;
  background: none;
  cursor: pointer;
  padding: 0;
  margin: -2px 0 0;
  font-size: 18px;
  line-height: 1;
  color: inherit;
  opacity: 0.6;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.ds-flash-close:hover {
  opacity: 1;
}

/* Read-only key/value row (bin inspector's Width×depth/Height/Finger scoop) —
   ported from the mock's .spec-row/.section-divider, which live in the ui
   kit's own page styles rather than the shared component library. */
.ds-spec-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 0;
  border-bottom: var(--border-width) solid var(--border-subtle);
}

.ds-spec-row:last-of-type {
  border-bottom: none;
}

.ds-spec-label {
  font-size: 13px;
  color: var(--text-secondary);
  font-weight: 600;
}

.ds-spec-value {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-primary);
  font-weight: 500;
}

.ds-divider {
  border: none;
  border-top: var(--border-width) solid var(--border-subtle);
  margin: var(--space-5) 0;
}

/* Color-swatch radio group: a real (visually-hidden) radio per swatch, so
   the selected state comes from :checked rather than a server-rendered
   "selected" class or JS — just a border-color change, matching the mock's
   .color-swatch[data-active]. */
.ds-swatch-option {
  position: relative;
  display: inline-flex;
  cursor: pointer;
}

.ds-swatch-option:has(input:checked) .ds-swatch {
  border-color: var(--text-primary);
}

/* Compact hint directly under a spec-row group (bin inspector) — tighter
   than .ds-hint's default paragraph margin, matching the mock's spacing. */
.ds-hint-tight {
  margin-block-start: var(--space-2);
}

/* Dimension fieldset: unit toggle + one .ds-stepper per dimension. */
/* No border/padding of its own — this fieldset always lives inside a
   .ds-card (see drawers/_form.html.erb), so a second bordered box here
   would nest three boxes (card > fieldset > unit toggle pill) for one
   section. The legend carries the grouping instead. */
.ds-dimensions {
  border: none;
  padding: 0;
  margin: 0 0 var(--space-4);
}

.ds-dimensions > legend {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
  padding-inline: 0;
  margin-block-end: var(--space-3);
}

.ds-unit-toggle {
  display: inline-flex;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  border: var(--border-width-thick) solid var(--border-default);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  margin-block-end: var(--space-3);
}

/* .ds-stepper already has its own −/+ buttons, so the browser's native
   number spin buttons are pure redundant clutter — hide them everywhere. */
.ds-stepper input[type="number"] {
  appearance: textfield;
  -moz-appearance: textfield;
}

.ds-stepper input[type="number"]::-webkit-inner-spin-button,
.ds-stepper input[type="number"]::-webkit-outer-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

/* Dotted-paper backdrop wrapping the canvas toolbar/viewport/hints — the
   center pane's surrounding chrome ported from the mock's .canvas-wrap.
   Flush (no card radius) like the sidebar/inspector either side of it —
   the middle grid column's own background, not a floating panel. The
   Three.js viewport inside keeps its own existing look (see below), this
   only sets the backdrop it sits on. */
.drawer-canvas-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-6);
  background: var(--surface-sunken);
  background-image: radial-gradient(var(--border-default) 1.5px, transparent 1.5px);
  background-size: 22px 22px;
  min-block-size: 0;
  overflow-y: auto;
}

/* Keyboard shortcut hint row below the canvas — a compact floating card on
   the dotted paper, matching the mock's .instructions/kbd.key. Content-sized
   (not stretched full width) so it reads as a chip strip, not a banner. */
.drawer-shortcuts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  width: fit-content;
  max-inline-size: 100%;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-card);
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-md);
}

/* Static/status text sitting directly on the dotted-paper backdrop needs
   its own card background — otherwise the dot pattern reads straight
   through the text. */
.drawer-canvas-wrap > .ds-hint {
  width: fit-content;
  max-inline-size: 100%;
  margin: 0;
  padding: var(--space-2) var(--space-4);
  background: var(--surface-card);
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.drawer-canvas-wrap > .ds-hint:empty {
  display: none;
}

.drawer-shortcuts-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 13px;
  color: var(--text-secondary);
}

kbd.drawer-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 22px;
  block-size: 22px;
  padding: 0 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  color: var(--text-primary);
  background: var(--surface-card);
  border: var(--border-width) solid var(--border-strong);
  border-block-end-width: 2.5px;
  border-radius: 6px;
  box-shadow: 0 1px 0 var(--border-strong);
}

/* Drawer canvas: the Three.js viewport. The object list is the accessible
   equivalent with full feature parity, not a fallback — every action here
   has a matching button there. */
.drawer-canvas-viewport {
  block-size: min(72vh, 640px);
  border: var(--border-width-thick) solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  overflow: hidden;
}

/* On the builder page specifically, the canvas grows to fill whatever's
   left in .drawer-canvas-wrap after the badge row/hint/shortcuts, instead
   of the fixed 72vh/640px sizing above — that fixed size is for contexts
   where the viewport isn't the tallest thing on the page (export modal,
   bin preview). */
.drawer-canvas-wrap .drawer-canvas-viewport {
  flex: 1;
  min-block-size: 280px;
}

.drawer-canvas-viewport:focus-visible {
  outline: 3px solid var(--color-accent, currentColor);
  outline-offset: 2px;
}

/* Full-height sidebar rail: toolbar (Add Bin + capacity) stays put, the
   list scrolls independently. .drawer-sidebar's own height comes from
   .u-split-3 stretching it to match the canvas column. Flush panel (no
   card radius/all-around border) matching the mock's .sidebar — divided
   from the canvas by a border-right, not a gap. */
.drawer-sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  block-size: 100%;
  min-block-size: 0;
  background: var(--surface-card);
  border-right: var(--border-width) solid var(--border-subtle);
  padding: var(--space-5) var(--space-4);
}

/* Inspector panel: the mirror of .drawer-sidebar on the right — same flush
   full-height treatment, divided from the canvas by a border-left. Applied
   to the turbo-frame element itself so it holds the border/padding across
   every frame swap (empty state, placement inspector, etc). Scrolls
   internally (a long bin form shouldn't grow the whole builder shell). */
.drawer-inspector-panel {
  display: block;
  block-size: 100%;
  min-block-size: 0;
  overflow-y: auto;
  background: var(--surface-card);
  border-left: var(--border-width) solid var(--border-subtle);
  padding: var(--space-5) var(--space-4);
}

.drawer-inspector-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  block-size: 100%;
  min-block-size: 220px;
}

.drawer-sidebar-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: none;
}

.drawer-object-list-scroll {
  flex: 1;
  overflow-y: auto;
  min-block-size: 0; /* let this shrink and scroll instead of stretching the sidebar taller */
}

/* Drawer object list: a compact row per bin. */
.drawer-object-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.drawer-object-row[draggable="true"] {
  cursor: grab;
}

/* The whole row is one button — click anywhere to select this placement in
   the canvas and open its inspector. Plain button reset plus the old row's
   flex layout, now living here instead of on the <li>. */
.drawer-object-select {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  inline-size: 100%;
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  padding: var(--space-2);
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
}

.drawer-object-select:hover {
  background: var(--surface-sunken);
}

.drawer-object-row-selected .drawer-object-select {
  background: color-mix(in oklch, var(--color-info) 12%, var(--surface-card));
  border-color: var(--color-info);
}

.drawer-object-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1;
  min-inline-size: 0;
}

.drawer-object-swatch {
  flex: none;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}

/* Name and compact dims on one line, name truncating first. */
.drawer-object-headline {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}

.drawer-object-name {
  margin: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.drawer-object-dims {
  margin: 0 0 0 auto;
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

.drawer-object-hint {
  margin: 0;
}

/* Action row: labeled buttons (Rotate/Remove/Duplicate/Delete), a clean 2x2
   grid matching the mock. Each button_to renders its own <form> wrapper —
   display:contents drops that box from layout so the <button> itself is
   the grid item and stretches to fill its column (otherwise columns come
   out equal-width but the buttons inside stay text-width, reading as
   uneven/sloppy rather than a tidy grid). The "Place on canvas" button
   (shelved placements still get Duplicate/Delete alongside it) spans both
   columns on its own row instead of being stranded at half width — see
   .drawer-object-controls-span. Bins' Duplicate is a modal-controller
   wrapper (trigger + <dialog>) rather than a bare form, so it needs the
   same display:contents treatment to stay a grid item instead of a box. */
.drawer-object-controls {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
}

.drawer-object-controls form,
.drawer-object-controls-modal {
  display: contents;
}

.drawer-object-controls-span {
  grid-column: 1 / -1;
}

.ds-icon {
  width: 18px;
  height: 18px;
  display: block;
}

/* Drawer card thumbnail: a live top-down SVG of the drawer's placements,
   not a stored image — computed fresh every request (see
   DrawersHelper#drawer_layout_thumbnail), so it's never stale. */
/* Fixed aspect-ratio box regardless of the drawer's own proportions — the
   SVG's viewBox + preserveAspectRatio (see drawer_layout_thumbnail) scales
   to fit inside it, so every card in a grid row comes out the same height
   instead of following each drawer's own width/depth ratio. */
.drawer-thumb-link {
  display: block;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--radius-sm);
}

.drawer-thumb-svg {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  border: var(--border-width) solid var(--border-default);
  border-radius: var(--radius-sm);
}

.drawer-thumb-floor {
  fill: var(--surface-sunken);
}

.drawer-thumb-grid-line {
  stroke: var(--border-default);
  stroke-width: 0.05;
  opacity: 0.4;
}

.drawer-thumb-bin {
  opacity: 0.85;
}

/* Cycled by project item (DrawersHelper::THUMBNAIL_PALETTE_SIZE) so each
   kind of bin reads as its own color across the thumbnail. Cherry is
   skipped — reserved for the invalid tint below. */
.drawer-thumb-bin-0 { fill: var(--cobalt-500); }
.drawer-thumb-bin-1 { fill: var(--terracotta-500); }
.drawer-thumb-bin-2 { fill: var(--teal-500); }
.drawer-thumb-bin-3 { fill: var(--pink-500); }
.drawer-thumb-bin-4 { fill: var(--gold-500); }

.drawer-thumb-bin.drawer-thumb-bin-invalid {
  fill: var(--color-danger);
}

/* Export modal: capped height (a long object list scrolls internally
   instead of growing the dialog past the viewport) and wide enough for a
   2-column thumbnail grid. The title stays put; the turbo-frame — which
   wraps everything below it, see exports/_modal.html.erb — is the
   scrolling region. Custom elements default to display:inline, so
   turbo-frame needs an explicit display to behave as a flex child.
   Scoped to [open] — dialog:not([open]) { display: none } is a UA-
   stylesheet rule, and author CSS overrides UA rules regardless of
   specificity, so an unconditional `display: flex` here would defeat the
   browser's own hidden-when-closed behavior. */
.export-dialog[open] {
  max-width: 640px;
  display: flex;
  flex-direction: column;
  max-block-size: min(85vh, 700px);
}

.export-dialog > turbo-frame {
  display: block;
  overflow-y: auto;
  min-block-size: 0;
}

.export-thumb-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-4);
}

/* Shrinks the shared .drawer-canvas-viewport sizing (72vh/640px, sized for
   the full drawer builder page) down to something that fits inside the
   now height-capped export dialog alongside its name/dims/download text. */
.export-preview-viewport {
  block-size: min(45vh, 360px);
}

/* Add Bin modal: same height-capping idea as .export-dialog, but a fixed
   block-size (not just a max) — the three tabs' content wildly differs in
   length (a big grid of cards vs. a single empty-state card vs. a form),
   and a size that changes per tab reads as janky. The tab list stays put;
   .add-bin-panels is the scrolling region for whichever tab is showing. */
.add-bin-dialog[open] {
  max-width: 760px;
  display: flex;
  flex-direction: column;
  block-size: min(80vh, 640px);
}

.add-bin-dialog .add-bin-tabs-wrap {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-block-size: 0;
}

.add-bin-panels {
  flex: 1;
  min-block-size: 0;
  overflow-y: auto;
}

/* New Bin panel only: the preview shouldn't scroll away with the fields
   just because tab/scoop toggled on an extra row — the other two tabs are
   happy to scroll as a single unit via .add-bin-panels above, but here we
   want the field column to scroll on its own while the preview stays put.
   That takes an unbroken flex/height chain from this panel's own fixed
   block-size (so .add-bin-panels itself has nothing left to overflow) down
   to the field column: panel -> turbo-frame (custom elements default to
   display:inline, so it needs an explicit one to become a flex participant)
   -> form -> .bin-form-layout -> .bin-form-fields. :not([hidden]) matters —
   author CSS beats the UA [hidden] { display: none } rule regardless of
   specificity, so an unconditional display here would make every tab's
   panel visible at once. */
.add-bin-panel-new:not([hidden]) {
  display: flex;
  flex-direction: column;
  block-size: 100%;
  min-block-size: 0;
}

.add-bin-panel-new > turbo-frame {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-block-size: 0;
}

/* bins/new.html.erb wraps the form in two plain divs (bin-new-wrap,
   bin-new-card — a back-link/heading and a card border in the standalone
   page, neither wanted inside the modal) that would otherwise break the
   height chain above: a flex child's flex:1 does nothing unless its own
   parent is itself a flex container, and plain divs aren't. display:
   contents removes them from the box tree (their CSS selectors — e.g.
   .u-stack's own spacing rule — still match against the DOM either way)
   so the form becomes a direct flex child of turbo-frame. */
.add-bin-panel-new .bin-new-wrap,
.add-bin-panel-new .bin-new-card {
  display: contents;
}

.add-bin-dialog form {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-block-size: 0;
}

.add-bin-dialog .bin-form-layout {
  flex: 1;
  min-block-size: 0;
  align-items: stretch;
}

.add-bin-dialog .bin-form-fields {
  max-block-size: 100%;
  overflow-y: auto;
  min-block-size: 0;
}

/* Safety net, not the common case: .bin-preview-viewport has its own fixed
   block-size (min(45vh, 360px)), which normally fits comfortably in the
   row alongside the scrolling field column above. Only a very short
   viewport could make the row itself shorter than that — cap and scroll
   here too rather than let the preview overflow past the row and overlap
   the Create/Cancel buttons below it. */
.add-bin-dialog .bin-form-preview {
  max-block-size: 100%;
  overflow-y: auto;
  min-block-size: 0;
}

/* Bin form's submit/cancel row — a divider so it reads as a distinct
   footer instead of running straight into the last field's hint text
   (matters most in the Add Bin modal, where this row sits pinned at the
   bottom of a scrolling column). */
.bin-form-actions {
  padding-block-start: var(--space-4);
  margin-block-start: var(--space-4);
  border-block-start: var(--border-width) solid var(--border-subtle);
}

/* Two-column field+preview row shared by the New Bin tab and the
   standalone bins/new /edit pages (same partial). Outside the modal there's
   no height constraint above to stretch into, so this is just top-aligned
   columns — the modal-scoped rules above are what turn on the split-scroll
   behavior. */
.bin-form-layout {
  gap: var(--space-6);
  align-items: flex-start;
}

/* .ds-field defaults to align-items: stretch (flex-direction: column with
   no align-items set), which stretches .ds-stepper to the field's full
   width even though .ds-stepper itself (display: inline-flex) is only as
   wide as its −/input/+ content — leaving a dead gap after the + button.
   Rather than fall back to pure content-sizing (which makes a stack of
   steppers come out three different widths depending on whether their
   unit label reads "u" or "units"), give every stepper the same fixed
   width and let the number input absorb it. Also match .ds-input's corner
   radius/height so a stepper next to a text field in the same form reads
   as the same control family instead of two. */
.ds-field > .ds-stepper {
  align-self: flex-start;
  inline-size: 100%;
  max-inline-size: 15rem;
  border-radius: var(--radius-md);
}

.ds-field > .ds-stepper button,
.ds-field > .ds-stepper input {
  block-size: 44px;
}

.ds-field > .ds-stepper input {
  flex: 1;
  min-inline-size: 0;
  width: auto;
}

.ds-field > .ds-stepper .ds-stepper-unit {
  flex: none;
}

/* Bin form's live preview (bin_previews/_frame.html.erb, loaded by
   bin_preview_controller.js) — a real OpenSCAD-rendered STL, same viewer as
   the export modal. Sized like .export-preview-viewport so it reads
   consistently wherever a small live-preview canvas shows up, and fits
   comfortably inside the Add Bin modal's fixed-height "New bin" panel. */
.bin-preview-viewport {
  inline-size: 18rem;
  max-inline-size: 100%;
  block-size: min(45vh, 360px);
  border: var(--border-width-thick) solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  overflow: hidden;
}

/* Sliding pill behind the active tab, animated on click instead of the
   design system's instant .ds-tab-active background swap — the active
   tab's own background/shadow are suppressed here so the two don't double
   up; see tabs_controller.js for the position/width math. */
.ds-tabs {
  position: relative;
}

.ds-tab-indicator {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 0;
  width: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  box-shadow: var(--shadow-sm);
  transition: left var(--duration-base) var(--ease-out), width var(--duration-base) var(--ease-out);
  pointer-events: none;
}

.ds-tabs .ds-tab {
  position: relative;
  z-index: 1;
}

.ds-tabs .ds-tab.ds-tab-active {
  background: transparent;
  box-shadow: none;
}

/* Export modal: each thumbnail is one plain-reset button styled like a
   .ds-card — click anywhere on it to open that object's 3D preview, same
   "whole card is the button" pattern as .drawer-object-select. */
.export-thumb-trigger {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  inline-size: 100%;
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--surface-card);
  padding: var(--space-4);
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}

.export-thumb-trigger:hover {
  background: var(--surface-sunken);
}

/* ============================================================
   Marketing: pre-launch teaser/waitlist landing page. Ported from the
   "3Drawers Studio design system" claude.ai project
   (ui_kits/marketing/teaser.html). Page-specific chrome only — buttons,
   inputs, etc. reuse the existing .ds-* component classes. Classes here
   are namespaced (marketing-*) rather than the source's bare .wrap/.top/
   footer selectors, which would otherwise apply app-wide.
   ============================================================ */
.marketing-wrap {
  max-width: 880px;
  margin: 0 auto;
  padding: 0 32px;
}

nav.marketing-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 22px 32px;
}

.marketing-lockup {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}

.marketing-stackmark {
  position: relative;
  width: 34px;
  height: 30px;
}

.marketing-stackmark .t {
  position: absolute;
  width: 18px;
  height: 18px;
  border-radius: 6px;
}

.marketing-stackmark .t1 { background: var(--gold-500); top: 9px; left: 9px; transform: rotate(-6deg); }
.marketing-stackmark .t2 { background: var(--teal-500); top: 4.5px; left: 4.5px; transform: rotate(4deg); }
.marketing-stackmark .t3 { background: var(--terracotta-500); top: 0; left: 0; transform: rotate(-2deg); }

.marketing-wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 19px;
  color: var(--text-primary);
}

.hero-band {
  position: relative;
  overflow: hidden;
  background: var(--neutral-800);
  color: #fff;
  padding: 0;
}

.hero-icon-field { position: absolute; inset: 0; z-index: 0; }
.hero-icon { position: absolute; }
.hero-icon svg { display: block; width: 100%; height: 100%; }

.hero-band::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 460px 340px at 50% 45%, rgba(27,27,34,.82) 0%, rgba(27,27,34,.5) 55%, rgba(27,27,34,0) 80%);
  z-index: 0;
}

.hero-band-inner {
  position: relative;
  z-index: 1;
  padding: 92px 32px 96px;
  max-width: 720px;
  margin: 0 auto;
  text-align: center;
}

.hero-band h1 {
  font-family: var(--font-display);
  font-size: 56px;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 16px;
}

.hero-band .eyebrow {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--gold-300);
  background: rgba(255,255,255,.08);
  border: 1.5px solid rgba(255,255,255,.2);
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  margin-bottom: 22px;
}

.signup-card {
  position: relative;
  z-index: 1;
  max-width: 480px;
  margin: -56px auto 0;
  background: var(--surface-card);
  color: var(--text-primary);
  border-radius: var(--radius-xl);
  padding: 30px 32px;
  box-shadow: var(--shadow-lg);
  border: var(--border-width) solid var(--border-subtle);
}

.signup-row { display: flex; gap: 10px; }
.signup-row .ds-input { flex: 1; }

.signup-success { display: flex; align-items: center; gap: 12px; padding: 6px 2px; }
.signup-success[hidden] { display: none; }

section.copy { padding: 76px 0; }

.copy-block {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 56px;
  align-items: center;
  margin-bottom: 88px;
}

.copy-block:last-child { margin-bottom: 0; }
.copy-block.reverse .copy-text { order: 2; }
.copy-block.reverse .copy-media { order: 1; }

.copy-text .kicker {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  font-weight: 700;
  margin-bottom: 10px;
  display: inline-block;
  padding: 4px 12px;
  border-radius: var(--radius-pill);
}

.copy-text h2 {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  margin: 0 0 14px;
  line-height: var(--leading-tight);
}

.copy-text p {
  font-size: var(--text-md);
  color: var(--text-secondary);
  line-height: var(--leading-relaxed);
  margin: 0;
}

.copy-media {
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  aspect-ratio: 4/3;
}

.copy-media img { display: block; width: 100%; height: 100%; object-fit: cover; }

.copy-media-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: var(--surface-sunken);
  border: var(--border-width) dashed var(--border-strong);
  color: var(--text-tertiary);
  font-size: var(--text-sm);
  text-align: center;
  padding: var(--space-4);
}

.cta-strip {
  background: var(--terracotta-500);
  color: #fff;
  text-align: center;
  padding: 64px 32px;
}

.cta-strip h2 { font-family: var(--font-display); font-size: var(--text-2xl); margin: 0 0 12px; }
.cta-strip p { font-size: var(--text-md); color: rgba(255,255,255,.85); margin: 0 0 30px; }
.cta-strip .signup-row { max-width: 440px; margin: 0 auto; }
.cta-strip .ds-input { background: rgba(255,255,255,.95); border-color: transparent; }

.marketing-footer {
  background: var(--neutral-900);
  color: rgba(255,255,255,.6);
  padding: 40px 0;
  font-size: 13px;
}

.marketing-footer .marketing-wrap {
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: var(--container-max);
}

.marketing-footer a { color: rgba(255,255,255,.8); }

@media (max-width: 44rem) {
  .copy-block, .copy-block.reverse {
    grid-template-columns: 1fr;
  }
  .copy-block.reverse .copy-text, .copy-block.reverse .copy-media {
    order: initial;
  }
  .hero-band h1 { font-size: 36px; }
  .signup-row { flex-direction: column; }
}
