/* 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;
}

/* Stretched-link cards (project/group index cards, etc.) — the title
   anchor's ::after is blown up to cover the whole card via inset:0, so
   clicking anywhere (thumbnail, badge, description) follows the link,
   not just the underlined title text. Badges and other controls inside
   stay visually interactive but aren't separate tab stops, so this only
   works for cards whose only actionable content is the one link. */
.ds-card-linked {
  position: relative;
  transition: transform var(--duration-fast) var(--ease-bounce),
    box-shadow var(--duration-fast) var(--ease-bounce),
    border-color var(--duration-fast) var(--ease-out);
}

/* The whole card is the click target (see the stretched-link ::after
   below) but only the title text ever reacted to hover — give the card
   itself the same lift/shadow/border feedback as .project-type-option so
   it reads as one clickable unit. */
.ds-card-linked:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--color-primary);
}

.ds-card-linked:active {
  transform: translateY(1px);
  box-shadow: var(--shadow-press);
}

.ds-card-linked h2 a::after,
.ds-card-linked h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* The title anchor's own focus ring only wraps its text, which reads as
   wrong on a card-sized click target — draw the ring around the whole
   card instead when that anchor has keyboard focus. */
.ds-card-linked h2 a:focus-visible,
.ds-card-linked h3 a:focus-visible {
  outline: none;
}

.ds-card-linked:has(h2 a:focus-visible),
.ds-card-linked:has(h3 a:focus-visible) {
  outline: var(--border-width-thick) solid var(--color-info);
  outline-offset: 2px;
}

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-logo {
  block-size: 26px;
  inline-size: auto;
  flex: none;
}

.app-nav-logo-dark { display: none; }
[data-theme="dark"] .app-nav-logo-light { display: none; }
[data-theme="dark"] .app-nav-logo-dark { display: block; }

.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. */
/* A modal <dialog> is promoted to the top layer and centered by the
 * browser's own auto-margin algorithm, independent of its DOM parent's
 * flow — except when a parent utility class still manages to set an
 * explicit margin on it. `.u-stack > * + *` (utilities.css) does exactly
 * that: any dialog that isn't its container's first child gets
 * margin-block-start pinned to a fixed value instead of auto, so the
 * centering math lopsides and every bit of vertical margin piles into
 * margin-bottom, pinning the dialog to the top of the viewport. Pin both
 * block margins back to auto with a selector specific enough to always
 * win, so every dialog centers the same way no matter where it sits in
 * the DOM. */
dialog.ds-dialog {
  margin-block: auto;
}

.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;
  }
}

/* The design system's dialog close button has no hover/active state of its
 * own — match the .ds-btn-icon treatment (lift + slight scale) so every
 * icon-only control in the app responds to hover/press the same way. */
.ds-dialog-close {
  transition: transform var(--duration-fast) var(--ease-bounce);
}

.ds-dialog-close:hover {
  transform: translateY(-2px) scale(1.05);
}

.ds-dialog-close:active {
  transform: scale(0.94);
}

/* 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);
}

/* base.css never resets the browser's default <p> margin, so .ds-hint (and
 * .ds-hint-error) carry ~1em of their own top/bottom margin on top of
 * .ds-field's 6px internal gap — control-to-hint spacing reads much looser
 * than the 6px the design system actually intends. */
.ds-field > .ds-hint {
  margin: 0;
}

/* Inside a tight group (e.g. the bin form's Width/Depth + Force Half Grid),
 * let .u-stack--tight's own rhythm be the only source of spacing between
 * fields — form .ds-field's blanket margin-block-end above would otherwise
 * stack on top of it between every field, undoing the "tight" grouping. */
.u-stack--tight .ds-field {
  margin-block-end: 0;
}

/* 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);
}

/* Width/depth/height side by side instead of stacked (see
   projects/_form.html.erb) — cuts a lot of height off the drawer-based
   project form, which otherwise runs much taller than every other step in
   the new-project dialog. Back to one column once a row of three steppers
   plus their hints can't fit without wrapping awkwardly. */
.ds-dimensions-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-4);
}

/* Grid items default to a min-width equal to their content's min-content
   size, which for a .ds-stepper (a non-wrapping flex row of button/input/
   button) is its full rendered width — without this, a 1fr track can't
   actually shrink to fit and the third column overflows the dialog. */
.ds-dimensions-row > .ds-field {
  min-width: 0;
}

@media (max-width: 34rem) {
  .ds-dimensions-row {
    grid-template-columns: 1fr;
  }
}

/* .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 */
}

/* The sidebar list is split into titled groups — "Bins" and "Accessories"
   (see projects/_object_list_section.html.erb). Both render even when
   empty, so the two stay in a stable order as things come and go. */
.drawer-object-group + .drawer-object-group {
  margin-block-start: var(--space-5);
}

.drawer-object-group-title {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
}

.drawer-object-group-empty {
  margin: 0;
}

/* 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%;
  /* Nothing in the app resets box-sizing to border-box (not even <html>),
     so without this, inline-size: 100% sizes only the content box and the
     border/padding below get added on top — the row renders ~19px wider
     than its actual slot in the sidebar, which .drawer-object-list-scroll's
     overflow-y: auto (which forces overflow-x to compute as auto too)
     then silently clips instead of visibly overflowing. */
  box-sizing: border-box;
  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;
  /* A flex item's default min-inline-size is its content's full nowrap
     width, not 0 — without this, the ellipsis/overflow above never
     actually engages, and the row grows past the sidebar's column width
     instead of shrinking, pushing .drawer-object-dims off the edge (or, in
     a grid column, blowing out the track width entirely so the canvas
     pane's background paints over the overflow). */
  min-inline-size: 0;
}

.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;
}

/* The New Lid tab has no preview column, so its field column is a direct
   flex child of the form instead of sitting inside .bin-form-layout — it
   needs the flex: 1 that .bin-form-layout otherwise supplies, or the
   overflow-y: auto above has no height to scroll within. */
.add-bin-dialog form > .bin-form-fields {
  flex: 1;
}

/* The "New item" tab's bin-or-lid chooser (projects/_item_type_picker):
   .add-bin-panel-new stretches its frame to full height, so keep the cards
   at their natural size at the top rather than stretched down the panel. */
.add-bin-panel-new .item-type-picker {
  align-content: start;
  overflow-y: auto;
  min-block-size: 0;
}

/* Radio group for the lid's top style — a plain fieldset, so it needs the
   usual border/padding/margin reset before the .ds-radio rows inside it
   read as a normal stacked field. */
.lid-top-styles {
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
}

.lid-top-styles .ds-radio {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Library tab "Add" control: a native <details> disclosure (see
   project_items/_library_objects.html.erb) rather than a JS popover, so the
   name field reveals in place without a second nested <dialog>. */
.library-object-add > summary {
  cursor: pointer;
  list-style: none;
}

.library-object-add > summary::-webkit-details-marker {
  display: none;
}

.library-object-add-form {
  margin-block-start: var(--space-3);
}

/* 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. */
/* .u-cluster.bin-form-layout, not just .bin-form-layout — both classes are
   on the same element already, but a bare .bin-form-layout only ties
   .u-cluster's own align-items: center on specificity, so which one
   actually wins depends on stylesheet load order (it was losing). The
   compound selector unambiguously outranks it. */
.u-cluster.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 field labels (Name/Width/Depth/…) — match the Preview column's
   heading, which gets --font-display for free from the global h1-h3 rule
   below; .ds-label (a plain <label>, not a heading) doesn't. Scoped to
   this form rather than every .ds-label site-wide, since that's a bigger
   call than this particular pairing asked for. */
.bin-form-fields .ds-label {
  font-family: var(--font-display);
}

/* The Name field sits beside the Preview column's heading, which carries
   the browser's default heading margin (~12px, h1-h3 below only sets
   font/line-height) that .ds-label doesn't — without this Name reads a
   hair higher than Preview. */
.bin-form-fields > .ds-field:first-child {
  margin-block-start: var(--space-3);
}

/* Small switch variant — not in the ported design system (base.css's
   .ds-switch has no size modifiers), added for a sub-option like Force
   Half Grid that sits under Width/Depth and shouldn't read at the same
   visual weight as a primary field label. Mirrors .ds-btn-sm's naming. */
.ds-switch.ds-switch-sm {
  gap: 8px;
  font-size: var(--text-base); /* the track/thumb below are what got shrunk; the label reads fine at full size */
}

.ds-switch.ds-switch-sm input {
  width: 34px;
  height: 20px;
}

.ds-switch.ds-switch-sm .ds-track {
  width: 34px;
  height: 20px;
}

.ds-switch.ds-switch-sm .ds-track::after {
  width: 14px;
  height: 14px;
}

.ds-switch.ds-switch-sm input:checked + .ds-track::after {
  transform: translateX(14px);
}

/* Advisory hint variant — base.css's .ds-hint only has a plain and an
   -error color (see form .ds-field's neighbor rules above); this is a
   third, non-blocking tier for "worth knowing, not wrong" copy like the
   bin form's print-splits-into-N-pieces notice. Warning-gold, not danger-
   red: the bin is still valid, just prints as multiple glued pieces. */
.ds-hint-warning {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  color: var(--color-warning-ink);
}

.ds-hint-warning .ds-icon {
  flex: none;
  width: 14px;
  height: 14px;
  color: var(--color-warning);
  margin-block-start: 1px;
}

/* 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;
}

/* The status text below it — a validation error (_frame.html.erb) or a
   load failure (stl_preview_controller.js#showError) — can be a full
   sentence. Nothing in this column's flex/content-sized ancestor chain
   otherwise constrains its width, so an unbroken line would stretch the
   whole Add Bin dialog instead of wrapping; match it to the viewport's own
   column width so it wraps there instead. */
#bin-preview p.ds-hint {
  max-inline-size: 18rem;
  overflow-wrap: break-word;
}

/* 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;
}

/* The design system gives the active tab a background/shadow but never
 * styled inactive tabs for hover — without this they're dead-looking next
 * to the sliding indicator. */
.ds-tabs .ds-tab:not(.ds-tab-active):hover {
  color: var(--text-primary);
}

/* 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: the community landing page, per the "3drawers Community
   Landing Page" Figma (file aeVbSAgDK2tXncaNwBa0of, frames "Desktop 1440 —
   Community LP" and "… — Dark Mode").

   Everything here is namespaced `.mk-*` and scoped under `body.mk`, because
   the marketing site and Studio deliberately do NOT share a look: marketing
   is skeuomorphic (paper, scattered desk objects, Solway + Space Grotesk),
   Studio is the flat `.ds-*` system (Grandstander + Plus Jakarta Sans). The
   `--mk-*` custom properties below are the marketing palette straight off
   the Figma frames and shadow nothing in tokens/.

   The scattered hero objects are photo cut-outs in the Figma. The file has
   exporting disabled, so they're stand-in SVG doodles for now — swap the
   <svg> inside each .mk-object for an <img> and the layout, drop-in and
   parallax all keep working unchanged.
   ============================================================ */
body.mk {
  /* Light — "Desktop 1440 — Community LP" */
  --mk-bg: #FFEAAC;
  --mk-ink: #2A0251;
  --mk-ink-strong: #110229;
  --mk-muted: rgba(26, 13, 63, .702);
  --mk-faint: rgba(26, 13, 63, .5);
  --mk-hairline: rgba(26, 13, 63, .122);
  --mk-eyebrow: #F29203;
  --mk-badge: #AA2D60;

  --mk-nav-bg: linear-gradient(180deg, rgba(255, 230, 157, .75), rgba(255, 220, 115, .55));
  --mk-nav-border: rgba(42, 2, 81, .08);

  --mk-card-bg: #FFFFFF;
  --mk-card-border: rgba(26, 13, 63, .122);
  --mk-field-bg: rgba(26, 13, 63, .031);
  --mk-field-border: rgba(26, 13, 63, .122);
  --mk-placeholder: rgba(26, 13, 63, .4);

  --mk-btn-from: #BAE85E;
  --mk-btn-to: #7AB200;
  --mk-btn-ink: #1A0D3F;

  --mk-paper: #F6E8AE;
  --mk-paper-rule: rgba(90, 138, 159, .38);
  --mk-paper-margin: rgba(200, 47, 105, .45);
  --mk-paper-ink: #2A0251;

  --mk-folder: #BDE0F0;
  --mk-folder-tab: #A8CFE0;
  --mk-folder-tab-ink: #5A8A9F;
  --mk-folder-ink: #110229;
  --mk-folder-muted: rgba(26, 13, 63, .702);
  --mk-slot: #FAF8F3;
  --mk-print: #FFFFFF;

  --mk-shadow-card: 0 24px 48px -20px rgba(42, 2, 81, .28);
  --mk-shadow-paper: 0 30px 60px -30px rgba(42, 2, 81, .45);

  /* Header pill (76px) + its 16px top offset + 16px breathing room; folder
     panels stick just below it so the accumulated tab row never hides
     under the nav. */
  --mk-sticky-top: 108px;
  --mk-container: 1204px;

  background: var(--mk-bg);
  color: var(--mk-ink);
  font-family: "Space Grotesk", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

[data-theme="dark"] body.mk {
  /* Dark — "Desktop 1440 — Community LP — Dark Mode" */
  --mk-bg: #110924;
  --mk-ink: #F4F0FC;
  --mk-ink-strong: #FFFFFF;
  --mk-muted: #A89EB7;
  --mk-faint: rgba(168, 158, 183, .7);
  --mk-hairline: rgba(255, 255, 255, .0824);
  --mk-eyebrow: #F29203;
  --mk-badge: #FF83B6;

  --mk-nav-bg: linear-gradient(180deg, rgba(28, 17, 54, .75), rgba(17, 9, 36, .85));
  --mk-nav-border: rgba(255, 255, 255, .0824);

  --mk-card-bg: linear-gradient(180deg, rgba(44, 27, 84, .92), rgba(28, 17, 54, .82));
  --mk-card-border: rgba(255, 255, 255, .0824);
  --mk-field-bg: rgba(255, 255, 255, .05);
  --mk-field-border: rgba(255, 255, 255, .0824);
  --mk-placeholder: rgba(244, 240, 252, .4);

  --mk-paper: #6B6152;
  --mk-paper-rule: rgba(20, 12, 40, .32);
  --mk-paper-margin: rgba(120, 30, 60, .5);
  --mk-paper-ink: #F4F0FC;

  --mk-folder: #281457;
  --mk-folder-tab: #180E30;
  --mk-folder-tab-ink: #A89EB7;
  --mk-folder-ink: #F4F0FC;
  --mk-folder-muted: #A89EB7;
  --mk-slot: #150B33;
  --mk-print: #E7E1F5;

  --mk-shadow-card: 0 24px 48px -20px rgba(0, 0, 0, .6);
  --mk-shadow-paper: 0 30px 60px -30px rgba(0, 0, 0, .7);
}

/* Nothing in the app resets box-sizing (see .drawer-object-select above), but
   every number on this page is a Figma measurement, and Figma sizes the
   border box — a 48px field with 4px padding and a 1px border has to come
   out 48px tall, not 58. Scoped to the marketing page so Studio's layouts,
   which were written against content-box, are untouched. */
body.mk,
body.mk *,
body.mk *::before,
body.mk *::after { box-sizing: border-box; }

/* The app shell makes <main> the scroll container (see the `main` rule near
   the top of this file) so the drawer builder can lock itself to one
   viewport. The marketing page scrolls on the document instead — it has to,
   because sticky elements resolve against their nearest scrollport, and
   inside an overflow:auto ancestor that never actually scrolls they simply
   never stick. That's the whole folder-stack effect, so undo both here. */
body.mk { display: block; min-block-size: 0; }
body.mk > main { display: block; flex: none; overflow: visible; min-block-size: 0; }

.mk a { color: inherit; }

.mk-shell {
  max-width: calc(var(--mk-container) + 48px);
  margin: 0 auto;
  padding: 0 24px;
}

/* Visually hidden but focusable — the skip link. */
.mk-skip {
  position: absolute;
  left: 16px;
  top: -100px;
  z-index: 100;
  padding: 10px 16px;
  border-radius: 10px;
  background: var(--mk-card-bg);
  color: var(--mk-ink);
  font-weight: 700;
  transition: top .15s ease;
}

.mk-skip:focus { top: 16px; }

/* ---------- Header ---------------------------------------------------- */
.mk-header {
  position: sticky;
  top: 16px;
  z-index: 50;
  margin: 24px auto 0;
  max-width: calc(var(--mk-container) + 48px);
  padding: 0 24px;
}

.mk-header-bar {
  display: flex;
  align-items: center;
  gap: 24px;
  min-height: 76px;
  padding: 0 14px 0 24px;
  border-radius: 20px;
  border: 1px solid var(--mk-nav-border);
  background: var(--mk-nav-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 10px 30px -18px rgba(42, 2, 81, .5);
}

.mk-lockup {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  font-family: "Solway", Georgia, serif;
  font-size: 24px;
  color: var(--mk-ink-strong);
  flex: none;
}

.mk-lockup img { display: block; height: 32px; width: auto; }
.mk-lockup .mk-logo-dark { display: none; }
[data-theme="dark"] .mk-lockup .mk-logo-light { display: none; }
[data-theme="dark"] .mk-lockup .mk-logo-dark { display: block; }

.mk-nav {
  display: flex;
  align-items: center;
  gap: 28px;
  margin: 0 auto;
}

.mk-nav a {
  text-decoration: none;
  font-size: 15px;
  font-weight: 500;
  color: var(--mk-ink);
  padding: 6px 2px;
  border-radius: 6px;
}

.mk-nav a:hover { text-decoration: underline; text-underline-offset: 4px; }

.mk-header-actions { display: flex; align-items: center; gap: 8px; flex: none; }

/* ---------- Buttons --------------------------------------------------- */
.mk-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 0;
  border-radius: 999px;
  padding: 12px 22px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  color: var(--mk-btn-ink);
  background: linear-gradient(180deg, var(--mk-btn-from), var(--mk-btn-to));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5), 0 6px 14px -8px rgba(122, 178, 0, .9);
  cursor: pointer;
  text-decoration: none;
  transition: filter .15s ease, transform .15s ease;
}

.mk-btn:hover { filter: brightness(1.06); }
.mk-btn:active { transform: translateY(1px); }
.mk-btn:disabled { opacity: .6; cursor: default; }

.mk-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--mk-nav-border);
  background: transparent;
  color: var(--mk-ink);
  cursor: pointer;
}

.mk-icon-btn:hover { background: rgba(42, 2, 81, .06); }
[data-theme="dark"] .mk-icon-btn:hover { background: rgba(255, 255, 255, .08); }

.mk :focus-visible {
  outline: 3px solid var(--mk-eyebrow);
  outline-offset: 2px;
}

/* ---------- Hero ------------------------------------------------------ */
/* Laid out to the Figma's frame coordinates. The negative top margin pulls
   the hero's box up under the header (which keeps its own space in flow and
   paints over at z-index 50) so the hero's top edge is the *frame's* top
   edge — objects that run off the top of the design, like the receipt and
   the chopsticks, need that origin to sit where they do in the Figma. Every
   padding below is a measured frame offset: badge top 166, card top 481,
   paper section top 1034. */
.mk-hero {
  position: relative;
  margin-top: -100px;
  padding: 166px 24px 235px;
  text-align: center;
  overflow: clip;
}

.mk-hero-head { position: relative; z-index: 4; max-width: 1052px; margin: 0 auto; }

.mk-badge {
  display: inline-block;
  padding: 10px 20px;
  border: 2px solid var(--mk-badge);
  border-radius: 100px;
  font-family: "Solway", Georgia, serif;
  font-weight: 700;
  font-size: 14px;
  line-height: 17px;
  letter-spacing: -.02em;
  text-transform: uppercase;
  color: var(--mk-badge);
}

/* -4px: in the Figma the headline's text box starts 4px above the badge's
   bottom edge — the badge is positioned over the headline block, not stacked
   on top of it. */
.mk-hero h1 {
  margin: -4px 0 0;
  font-family: "Solway", Georgia, serif;
  font-weight: 400;
  font-size: clamp(40px, 6.7vw, 96px);
  line-height: 1.146;
  letter-spacing: -.031em;
  color: var(--mk-ink);
  text-wrap: balance;
}

/* The desk-object field: a stage in the Figma frame's own coordinates.
   It's exactly 1440 units wide, so an object's --x/--y/--w/--h can be the
   raw numbers off the Figma inspector and `1cqw = 14.4 design px` converts
   them. Below 1440 the whole scatter scales down with the viewport; above
   it the stage caps and centres, so objects never grow past design size on
   a wide monitor.

   container-type also makes this the containing block for the objects and
   applies layout containment, which does not clip — objects that hang off
   the stage edges still paint (the hero's own overflow: clip is what
   trims them). */
.mk-objects {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: min(100%, 1440px);
  translate: -50% 0;
  container-type: inline-size;
  pointer-events: none;
}

.mk-objects--back { z-index: 1; }
.mk-objects--front { z-index: 3; }

.mk-object {
  position: absolute;
  left: calc(var(--x) * 100cqw / 1440);
  top: calc(var(--y) * 100cqw / 1440);
  width: calc(var(--w) * 100cqw / 1440);
  height: calc(var(--h) * 100cqw / 1440);
  translate: 0 calc(var(--mk-scroll, 0px) * var(--p, .06));
  will-change: translate;
}

/* No rotation here: Figma reports the *bounding* box of a rotated layer and
   each PNG is that box exported at 2x, so the angle is already in the
   pixels. Rotating again would double it. */
.mk-object img {
  display: block;
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 12px 14px rgba(42, 2, 81, .18));
  animation:
    mk-drop .8s cubic-bezier(.2, .9, .3, 1.25) var(--d, 0s) both,
    mk-float 7s ease-in-out calc(var(--d, 0s) + 1s) infinite alternate none;
}

@keyframes mk-drop {
  from { transform: translateY(-90px) scale(.7); opacity: 0; }
  60%  { transform: translateY(6px) scale(1.04); opacity: 1; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}

@keyframes mk-float {
  from { transform: translateY(-5px); }
  to   { transform: translateY(5px); }
}

/* ---------- Signup card ----------------------------------------------- */
.mk-card {
  position: relative;
  z-index: 2;
  max-width: 680px;
  margin: 58px auto 0;
  padding: 48px;
  text-align: left;
  border: 1px solid var(--mk-card-border);
  border-radius: 24px;
  background: var(--mk-card-bg);
  box-shadow: var(--mk-shadow-card);
  scroll-margin-top: var(--mk-sticky-top);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.mk-eyebrow {
  margin: 0;
  font-size: 12px;
  line-height: 16px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--mk-eyebrow);
}

.mk-card h2 {
  margin: 12px 0 0;
  font-family: "Solway", Georgia, serif;
  font-weight: 400;
  font-size: 32px;
  line-height: 40px;
  color: var(--mk-ink-strong);
}

.mk-lede {
  margin: 12px 0 0;
  font-size: 16px;
  line-height: 24px;
  color: var(--mk-muted);
}

.mk-label {
  display: block;
  margin: 12px 0 4px;
  font-size: 14px;
  line-height: 20px;
  font-weight: 700;
  color: var(--mk-ink-strong);
}

/* Composite field: one 48px pill (Figma radius 100) holding the input, with
   the submit button inset 4px on three sides — the Figma pins the button
   past the field's own 16px padding, which is why only the left padding is
   16 here. The wrapper carries the focus ring so the button doesn't look
   detached when the input is focused. */
.mk-field {
  display: flex;
  align-items: center;
  gap: 8px;
  block-size: 48px;
  padding: 4px 4px 4px 16px;
  border: 1px solid var(--mk-field-border);
  border-radius: 100px;
  background: var(--mk-field-bg);
}

.mk-field:focus-within { outline: 3px solid var(--mk-eyebrow); outline-offset: 2px; border-radius: 100px; }
.mk-field input:focus-visible { outline: none; }

.mk-field input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 0;
  font-family: inherit;
  font-size: 15px;
  line-height: 27px;
  color: var(--mk-ink-strong);
}

/* 155 x 40 in the Figma — the height is what leaves the 4px inset inside a
   48px field, so it can't just come from the button's own padding. */
.mk-field .mk-btn { block-size: 40px; padding: 0 27px; }

.mk-field input::placeholder { color: var(--mk-placeholder); }

.mk-hint {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 16px 0 0;
  font-size: 12px;
  line-height: 16px;
  color: var(--mk-faint);
}

.mk-hint svg { flex: none; color: var(--mk-btn-to); }

/* display: flex above out-specifies the UA's [hidden] { display: none }, so
   without this the error line keeps its 16px margin while "hidden" — and
   would show its text outright once the controller filled it in. */
.mk-hint[hidden] { display: none; }

.mk-hint-error { color: var(--mk-badge); }
.mk-hint-error svg { color: currentColor; }

.mk-success { display: flex; align-items: center; gap: 14px; }
.mk-success[hidden] { display: none; }

.mk-success-mark {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background: linear-gradient(180deg, var(--mk-btn-from), var(--mk-btn-to));
  color: var(--mk-btn-ink);
}

.mk-success strong {
  display: block;
  font-family: "Solway", Georgia, serif;
  font-weight: 400;
  font-size: 22px;
  color: var(--mk-ink-strong);
}

.mk-success span { font-size: 14px; color: var(--mk-muted); }

/* ---------- Paper + folders ------------------------------------------- */
.mk-features { padding: 0 24px 120px; }

/* A sheet of ruled paper torn out of a pad and turned sideways, drawn in
   CSS (the Figma uses a photo of one). Rules run vertically because the
   sheet is turned; the red margin rule sits near the bottom; the mask
   scallops the bottom edge into a torn-out strip.

   The paper is a pseudo-element rather than the section's own background
   so the tilt and the mask stay off the content's ancestor chain — the
   folder panels inside are position: sticky, and keeping transforms and
   masks out from over them avoids any interaction with that. */
.mk-paper-sheet {
  position: relative;
  max-width: 1367px;
  margin: 0 auto;
  padding: 64px clamp(24px, 11.8vw, 170px) 96px;
}

.mk-paper-sheet::before {
  content: "";
  position: absolute;
  inset: -16px -10px -28px;
  z-index: 0;
  rotate: -.6deg;
  background:
    linear-gradient(var(--mk-paper-margin), var(--mk-paper-margin)) no-repeat 0 calc(100% - 92px) / 100% 1px,
    repeating-linear-gradient(90deg, transparent 0 35px, var(--mk-paper-rule) 35px 36px),
    linear-gradient(180deg, color-mix(in srgb, var(--mk-paper) 92%, white), var(--mk-paper));
  box-shadow: var(--mk-shadow-paper);
  -webkit-mask:
    linear-gradient(#000 0 0) top / 100% calc(100% - 34px) no-repeat,
    radial-gradient(circle at 50% 0, #000 68%, transparent 70%) bottom left / 46px 34px repeat-x;
  mask:
    linear-gradient(#000 0 0) top / 100% calc(100% - 34px) no-repeat,
    radial-gradient(circle at 50% 0, #000 68%, transparent 70%) bottom left / 46px 34px repeat-x;
}

.mk-paper-inner { position: relative; z-index: 1; }

.mk-features h2 {
  margin: 0 0 20px;
  font-family: "Solway", Georgia, serif;
  font-weight: 400;
  font-size: clamp(28px, 3.2vw, 40px);
  line-height: 1.18;
  color: var(--mk-paper-ink);
}

/* The stack. Every panel sticks at the same offset, so each one slides up
   over the one before it and the tab row is what stays visible — the
   filing-cabinet effect from the Figma comment thread (mutinyhq.com). */
/* Block flow, not a grid: a sticky item's travel is bounded by its
   containing block, and in a grid that's its own single-row track — zero
   room to stick. In normal flow every panel's containing block is the
   whole stack, so each one can ride up to the offset and stay there. */
/* The trailing space is what lets the LAST panel travel up to the sticky
   offset like the others — without it the stack ends mid-transition, with
   panel 3 still pinned above panel 4. */
.mk-folders { display: block; padding-block-end: 22vh; }

.mk-folder {
  position: sticky;
  margin-bottom: 96px;

  top: var(--mk-sticky-top);
  z-index: var(--i);
  scroll-margin-top: var(--mk-sticky-top);
}

.mk-folder:last-child { margin-bottom: 0; }

.mk-folder-tabs {
  display: flex;
  gap: 2px;
}

.mk-folder-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 38px;
  border-radius: 10px 10px 0 0;
  background: var(--mk-folder-tab);
  color: var(--mk-folder-tab-ink);
  font-size: 14px;
  font-weight: 700;
}

/* The current panel's own tab reads as one piece with the folder below it. */
.mk-folder-tab-current {
  background: var(--mk-folder);
  color: var(--mk-folder-ink);
}

.mk-folder-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
  padding: 40px;
  border-radius: 0 24px 24px 24px;
  background: var(--mk-folder);
  box-shadow: 0 18px 40px -24px rgba(42, 2, 81, .55);
  /* Every panel is the same height on purpose: they all stick at the same
     offset, so a taller earlier panel would poke out below the shorter
     later one that's supposed to have covered it. */
  min-block-size: 480px;
}

.mk-folder-media {
  border-radius: 16px;
  background: var(--mk-slot);
  overflow: hidden;
  aspect-ratio: 453 / 415;
  display: grid;
  place-items: center;
  padding: 12px;
}

.mk-folder-media > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 8px;
}

/* Two prints dropped on the slot rather than a tidy pair of tiles: the
   "after" lies over the "before", each tilted the other way, sharing the one
   caption underneath. Both photos are 3:4 as shot, so nothing is cropped.

   The grid lives on the <figure> because the caption is the images' sibling,
   not their wrapper — the two photos take a column each and the caption
   spans both. Its own padding is what keeps the tilted corners clear of the
   slot's overflow: hidden. */
.mk-media-pair { width: 100%; padding: 10px; }

.mk-media-pair figure {
  margin: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
}

.mk-media-pair img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border: 5px solid var(--mk-print);
  border-radius: 3px;
  box-shadow: 0 10px 20px -10px rgba(42, 2, 81, .55);
}

/* Each photo pulls 8% toward the middle, so they overlap by about a sixth
   of the figure without the pair drifting off-centre, and each sits a few
   pixels off the other's baseline so they read as tossed down rather than
   aligned. z-index needs no positioning here: grid items paint by z-index
   the way positioned boxes do, which is what puts the "after" on top. */
.mk-media-pair img:first-of-type {
  rotate: -4deg;
  margin-inline-end: -8%;
  translate: 0 -7px;
}

.mk-media-pair img:nth-of-type(2) {
  rotate: 3.5deg;
  margin-inline-start: -8%;
  translate: 0 7px;
  z-index: 1;
}

.mk-media-pair figcaption {
  grid-column: 1 / -1;
  margin-top: 16px;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--mk-folder-muted);
}

/* Folder 3 has no photograph yet — this is a drawn stand-in for one. */
.mk-tip-list { width: 100%; display: grid; gap: 10px; padding: 4px; }

.mk-tip-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-radius: 10px;
  background: var(--mk-folder);
  color: var(--mk-folder-ink);
  font-size: 14px;
  font-weight: 500;
}

.mk-tip-list svg { flex: none; color: var(--mk-btn-to); }

.mk-folder-copy { align-self: center; padding: 8px 8px 8px 0; }

.mk-folder-copy h3 {
  margin: 0;
  font-family: "Solway", Georgia, serif;
  font-weight: 400;
  font-size: clamp(22px, 2.3vw, 32px);
  line-height: 1.25;
  color: var(--mk-folder-ink);
}

.mk-folder-copy p {
  margin: 16px 0 0;
  font-size: 16px;
  line-height: 26px;
  color: var(--mk-folder-muted);
}

.mk-folder-copy a { text-decoration: underline; text-underline-offset: 3px; }

/* ---------- Footer ---------------------------------------------------- */
.mk-footer {
  border-top: 1px solid var(--mk-hairline);
  padding: 32px 0 56px;
  font-size: 13px;
  color: var(--mk-muted);
}

.mk-footer .mk-shell {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  justify-content: space-between;
  align-items: center;
}

.mk-footer nav { display: flex; gap: 20px; }
.mk-footer a { text-decoration: none; }
.mk-footer a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ---------- Responsive ------------------------------------------------ */
@media (max-width: 60rem) {
  .mk-nav { display: none; }
  /* One column makes panel heights swing too far apart for the same-height
     trick above to hold, so the stack becomes a plain vertical list. */
  .mk-folder { position: static; margin-bottom: 56px; }
  .mk-folders { padding-block-end: 0; }
  .mk-folder-body { grid-template-columns: 1fr; min-block-size: 0; padding: 24px; gap: 24px; }
  .mk-folder-media { aspect-ratio: 4 / 3; }
  /* The overlapping pair is taller than a landscape slot once the folder
     goes single-column, and the slot clips — let it hug the photos instead
     of cropping them. Matched on the content so the markup stays as-is. */
  .mk-folder-media:has(.mk-media-pair) { aspect-ratio: auto; }
  .mk-folder-copy { padding: 0 8px 8px; }
  /* The scatter is tuned for a wide hero; at phone widths the objects
     either land on the headline or get sliced in half by the viewport
     edge, so the field sits this one out. Worth revisiting once the real
     photo cut-outs land and a mobile scatter can be art-directed. */
  .mk-objects { display: none; }
}

@media (max-width: 40rem) {
  body.mk { --mk-sticky-top: 96px; }
  .mk-hero { padding: 32px 16px 72px; }
  .mk-card { padding: 28px 20px; border-radius: 20px; }
  .mk-card h2 { font-size: 26px; line-height: 34px; }
  /* Stacked, so the fixed 48px pill height has to go — the radius softens
     to match the taller box rather than reading as a stadium. */
  .mk-field { flex-direction: column; align-items: stretch; block-size: auto; padding: 8px; gap: 8px; border-radius: 24px; }
  .mk-field:focus-within { border-radius: 24px; }
  .mk-field input { padding: 10px 8px; }
  .mk-field .mk-btn { block-size: 44px; }
  .mk-paper-sheet { padding: 40px 16px 72px; }
  .mk-folder-media { aspect-ratio: 3 / 2; }
}

/* Only actual motion goes: the load-in bounce, the idle float and the
   scroll-linked drift. The folder stack stays — sticky pinning moves
   nothing on its own, exactly like a sticky header, and dropping it would
   take the section's structure with it. */
@media (prefers-reduced-motion: reduce) {
  .mk-object img { animation: none; }
  .mk-object { translate: none; }
}

/* New-project dialog (project_groups/show.html.erb): wider than the default
   .ds-dialog max-width so the type picker's two illustrated cards can sit
   side by side instead of the grid collapsing to one narrow column.
   Unscoped from [open] on purpose — dialog.close() removes the `open`
   attribute immediately, before the fade/scale-out transition finishes
   (see .ds-dialog's allow-discrete transition above), so an [open]-scoped
   width snaps back to the default 440px mid-animation and the two-column
   grid visibly collapses to one column while it's still fading out.
   min-height is tall enough to fit the tallest step (the drawer-based
   form, ~690px) without growing — every other step's content is centered
   in the leftover space (below) rather than pinned to the top with a dead
   gap under it. Bump min-height if that form grows a field. */
.new-project-dialog {
  max-width: 640px;
  min-height: 700px;
  display: flex;
  flex-direction: column;
}

/* Stretches the picker/form frame to fill the space min-height added, so
   .new-project-step-body has room to center its content within. Scoped to
   the dialog on purpose — projects/new.html.erb also renders this frame
   as a plain standalone page, which should keep its normal block layout. */
.new-project-dialog #new-project-picker {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.new-project-step-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Left/right slide between #new-project-picker's two steps (type picker <->
   form) — see modal_controller.js#animateFrame, which wraps the frame's
   render in document.startViewTransition and tags <html> with
   [data-frame-direction] via #markForward/#markBackward. Named so this is
   scoped to that one frame instead of the browser's default whole-document
   root transition. The frame's first load and the dismiss-triggered reset
   back to step one are deliberately excluded (see animateFrame's guards),
   so the dialog's own open/close animation is untouched. Browsers without
   View Transitions support (Safari/Firefox as of this writing) just get an
   instant swap — these rules have no effect there. */
#new-project-picker {
  view-transition-name: new-project-picker;
}

/* View-transition pseudo-elements render in their own top-layer overlay,
   independent of the dialog's DOM box — without this, the sliding old/new
   panels paint straight over the dialog's rounded corners instead of
   staying inside them. Clipping the group (not just the image pair) keeps
   both panels windowed to the frame's own box for the whole animation,
   like swiping between carousel pages. */
::view-transition-group(new-project-picker) {
  overflow: hidden;
}

::view-transition-image-pair(new-project-picker) {
  isolation: isolate;
}

::view-transition-old(new-project-picker),
::view-transition-new(new-project-picker) {
  animation-duration: var(--duration-base);
  animation-timing-function: var(--ease-out);
  mix-blend-mode: normal;
}

:root[data-frame-direction="forward"]::view-transition-old(new-project-picker) {
  animation-name: new-project-picker-slide-out-left;
}

:root[data-frame-direction="forward"]::view-transition-new(new-project-picker) {
  animation-name: new-project-picker-slide-in-right;
}

:root[data-frame-direction="backward"]::view-transition-old(new-project-picker) {
  animation-name: new-project-picker-slide-out-right;
}

:root[data-frame-direction="backward"]::view-transition-new(new-project-picker) {
  animation-name: new-project-picker-slide-in-left;
}

@keyframes new-project-picker-slide-out-left {
  to { transform: translateX(-100%); opacity: 0; }
}

@keyframes new-project-picker-slide-in-right {
  from { transform: translateX(100%); opacity: 0; }
}

@keyframes new-project-picker-slide-out-right {
  to { transform: translateX(100%); opacity: 0; }
}

@keyframes new-project-picker-slide-in-left {
  from { transform: translateX(-100%); opacity: 0; }
}

/* New-project type picker (projects#new with no project_type param yet —
   see projects/_type_picker.html.erb): big illustrated choice cards, the
   whole card a single link rather than a radio + separate label. */
.project-type-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-5);
}

.project-type-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-8) var(--space-6);
  background: var(--surface-card);
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  text-align: center;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-bounce),
    box-shadow var(--duration-fast) var(--ease-bounce),
    border-color var(--duration-fast) var(--ease-out);
}

.project-type-option:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--color-primary);
}

.project-type-option:active {
  transform: translateY(1px);
  box-shadow: var(--shadow-press);
}

.project-type-option h2 {
  margin: 0;
  font-size: var(--text-lg);
}

.project-type-option p {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.project-type-option-image {
  width: 96px;
  height: 96px;
}

/* ==========================================================================
   Onboarding survey (app/controllers/onboarding_controller.rb) — one
   question per full-page screen, Typeform-style. Content/branching lives in
   app/models/onboarding/questions.rb; animation + keyboard/auto-advance
   behavior in app/javascript/controllers/onboarding_controller.js.
   ========================================================================== */

.onboarding-body {
  background: var(--surface-page);
}

.onboarding-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.onboarding-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: var(--surface-sunken);
  z-index: 10;
}

.onboarding-progress-bar {
  height: 100%;
  background: var(--color-primary);
  transition: width var(--duration-slow) var(--ease-out);
}

turbo-frame#onboarding-step {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: 560px;
  width: 90%;
  margin: 0 auto;
  padding: var(--space-16) 0;
}

.onboarding-back {
  align-self: flex-start;
  color: var(--text-secondary);
  text-decoration: none;
  font-size: var(--text-sm);
  margin-bottom: var(--space-6);
}

.onboarding-back:hover {
  color: var(--text-primary);
}

.onboarding-step-count {
  color: var(--text-tertiary);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-4);
}

.onboarding-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.onboarding-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.onboarding-prompt {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  margin: 0;
}

.onboarding-hint {
  color: var(--text-secondary);
  margin: calc(-1 * var(--space-4)) 0 0;
}

.onboarding-error {
  color: var(--color-danger);
  font-size: var(--text-sm);
  margin: 0;
}

.onboarding-text-input,
.onboarding-textarea {
  font-size: var(--text-lg);
  width: 100%;
}

.onboarding-options {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.onboarding-option {
  padding: var(--space-4) var(--space-5);
  border: var(--border-width) solid var(--border-default);
  border-radius: var(--radius-md);
  justify-content: space-between;
  transition: border-color var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out);
}

.onboarding-option:hover {
  border-color: var(--color-primary);
  background: var(--surface-sunken);
}

.onboarding-option-label {
  flex: 1;
}

/* The Typeform-style "press 1-9 to pick this" badge — index-matched to
   optionTargets in onboarding_controller.js#handleKeydown. */
.onboarding-option-key {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  border: var(--border-width) solid var(--border-default);
  color: var(--text-tertiary);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.onboarding-other-text {
  display: none;
  margin-top: calc(-1 * var(--space-2));
}

/* Reveals the "somewhere else" free-text field when its checkbox is
   checked — plain CSS, no JS, per allow_browser versions: :modern. */
.onboarding-options-multi:has(#other_present:checked) .onboarding-other-text {
  display: block;
}

.onboarding-bed-size {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.onboarding-bed-size-custom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.onboarding-bed-size-custom .ds-field {
  flex: 1 1 120px;
}

.onboarding-bed-size-custom[hidden] {
  display: none;
}

/* bed_size presets auto-advance like boolean/single_select, but picking
   "custom" reveals fields that need an explicit Continue — hide the button
   until then, same :has() trick as .onboarding-other-text above. */
.onboarding-form:has(.onboarding-bed-size-custom[hidden]) .onboarding-actions {
  display: none;
}

.onboarding-slider {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) 0;
}

.onboarding-slider-output {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  color: var(--color-primary);
}

.onboarding-range {
  width: 100%;
}

.onboarding-slider-labels {
  display: flex;
  justify-content: space-between;
  width: 100%;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.onboarding-skip-form {
  margin: var(--space-4) auto 0;
}

/* Left/right slide between onboarding steps — same technique as
   #new-project-picker above (see modal_controller.js's animateFrame /
   markForward / markBackward, mirrored for onboarding in
   onboarding_controller.js), scoped to its own view-transition-name so the
   two never collide. */
#onboarding-step {
  view-transition-name: onboarding-step;
}

::view-transition-group(onboarding-step) {
  overflow: hidden;
}

::view-transition-image-pair(onboarding-step) {
  isolation: isolate;
}

::view-transition-old(onboarding-step),
::view-transition-new(onboarding-step) {
  animation-duration: var(--duration-base);
  animation-timing-function: var(--ease-out);
  mix-blend-mode: normal;
}

:root[data-frame-direction="forward"]::view-transition-old(onboarding-step) {
  animation-name: onboarding-step-slide-out-left;
}

:root[data-frame-direction="forward"]::view-transition-new(onboarding-step) {
  animation-name: onboarding-step-slide-in-right;
}

:root[data-frame-direction="backward"]::view-transition-old(onboarding-step) {
  animation-name: onboarding-step-slide-out-right;
}

:root[data-frame-direction="backward"]::view-transition-new(onboarding-step) {
  animation-name: onboarding-step-slide-in-left;
}

@keyframes onboarding-step-slide-out-left {
  to { transform: translateX(-100%); opacity: 0; }
}

@keyframes onboarding-step-slide-in-right {
  from { transform: translateX(100%); opacity: 0; }
}

@keyframes onboarding-step-slide-out-right {
  to { transform: translateX(100%); opacity: 0; }
}

@keyframes onboarding-step-slide-in-left {
  from { transform: translateX(-100%); opacity: 0; }
}

/* Admin section (/admin) — plain data tables and Q&A lists, no design-system
   component exists for either yet since only the admin panel needs them. */
.admin-subtle {
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.admin-table-wrap {
  overflow-x: auto;
  border: var(--border-width) solid var(--border-default);
  border-radius: var(--radius-md);
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface-card);
}

.admin-table th,
.admin-table td {
  padding: var(--space-3) var(--space-4);
  text-align: left;
  border-bottom: var(--border-width) solid var(--border-subtle);
  white-space: nowrap;
}

.admin-table th {
  background: var(--surface-sunken);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
}

.admin-table tbody tr:last-child td {
  border-bottom: none;
}

.admin-answer-list {
  display: grid;
  gap: var(--space-4);
}

.admin-answer {
  padding: var(--space-4);
  background: var(--surface-card);
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.admin-answer dt {
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

.admin-answer dd {
  color: var(--text-secondary);
  margin: 0;
}

.admin-answer-empty {
  font-style: italic;
  color: var(--text-tertiary);
}

.admin-library-handle {
  cursor: grab;
  color: var(--text-tertiary);
  touch-action: none;
}

.admin-library-category.sortable-ghost,
.admin-library-category .ds-card.sortable-ghost {
  opacity: 0.4;
}
