/* ==========================================================================
   Instant quote tool
   --------------------------------------------------------------------------
   Two states: the drop zone alone, then the drop zone shrunk with a viewer and
   an option panel beside it. Nothing here is loaded on any other page.
   ========================================================================== */

/* -- Drop zone ------------------------------------------------------------- */

.drop-zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-8) var(--sp-6);
  margin-top: var(--sp-6);
  border: 2px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  background: var(--paper);
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}

.drop-zone:hover,
.drop-zone:focus-visible,
.drop-zone.is-over {
  border-color: var(--flame-500);
  background: #fffaf4;
}

.drop-zone:focus-visible {
  outline: 2px solid var(--flame-500);
  outline-offset: 2px;
}

/* Once a model is loaded the zone stays available for a second file but stops
   dominating the page — the viewer is what matters from then on. */
.drop-zone.is-compact {
  padding: var(--sp-4);
  gap: 0;
}

.drop-zone.is-compact .drop-zone__icon,
.drop-zone.is-compact .drop-zone__privacy,
.drop-zone.is-compact .drop-zone__meta {
  display: none;
}

.drop-zone.is-compact .drop-zone__title {
  font-size: var(--text-sm);
  color: var(--ink-400);
}

.drop-zone__icon {
  width: 40px;
  height: 40px;
  color: var(--flame-500);
}

.drop-zone__title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-lg);
  margin: 0;
}

.drop-zone__meta {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-400);
}

/* The privacy line is a promise the code keeps — the file is parsed in the tab.
   If that ever changes, this copy changes in the same commit. */
.drop-zone__privacy {
  margin: var(--sp-2) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-400);
  max-width: 46ch;
}

/* -- Status ---------------------------------------------------------------- */

.quote-status {
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius);
  font-size: var(--text-sm);
  border: 1px solid transparent;
}

.quote-status--busy {
  background: var(--surface-alt);
  color: var(--ink-500);
}

.quote-status--warn {
  background: #fff6e6;
  border-color: #f2d9a8;
  color: #8a5a12;
}

.quote-status--error {
  background: var(--danger-100);
  border-color: #f0b8b8;
  color: var(--danger-600);
}

/* -- Workspace ------------------------------------------------------------- */

/*
 * Viewer on top at full width, controls in a row beneath it.
 *
 * Chosen by the owner on 2026-09-09 (layout 2 of the two prototypes; see
 * docs/archive/DESIGN-DECISIONS.md). It was the side-by-side 1.3fr/1fr grid before.
 *
 * The trade-off that came with this choice, and what is done about it: a
 * full-width viewer pushes the price down the page, and the price is the one
 * thing this page exists to show. Two rules below answer that, and neither may
 * be dropped without bringing the problem back.
 *
 *   1. The viewer is capped at 48vh, so the top of the panel — which is the
 *      price — is on screen on an ordinary laptop without scrolling.
 *   2. The price column is sticky, so it stays put while someone works down a
 *      long control column. Changing quantity and not being able to see what it
 *      did is the failure this prevents.
 */
.quote-workspace {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
  margin-top: var(--sp-6);
}

.quote-panel {
  display: grid;
  grid-template-columns: minmax(15rem, 0.95fr) minmax(15rem, 1fr) minmax(17rem, 1.15fr);
  gap: var(--sp-5);
  align-items: start;
}

/* One column at a time as the room runs out, rather than three cramped ones.
   The adjust group is the widest — it holds three number inputs on a row — so
   it is what decides when the three-column form stops fitting. */
@media (max-width: 1100px) {
  .quote-panel {
    grid-template-columns: 1fr 1fr;
  }
  .quote-panel__col--adjust {
    grid-column: 1 / -1;
  }
}

@media (max-width: 720px) {
  .quote-panel {
    grid-template-columns: 1fr;
  }
  .quote-panel__col--adjust {
    grid-column: auto;
  }
}

/*
 * The price follows you down the controls.
 *
 * `top` clears nothing in particular — the header is not fixed — so a small
 * gap is all it needs. Sticky only bites while the column's own row is in
 * view, which is exactly the span where the controls are being used.
 */
.quote-panel__col--price {
  position: sticky;
  top: var(--sp-4);
}

/* Sticky positioning is a scroll behavior, and on a short screen it turns into
   a block that never moves out of the way. Below this height the column simply
   flows. */
@media (max-height: 700px), (max-width: 720px) {
  .quote-panel__col--price {
    position: static;
  }
}

/* A neutral mid-light backdrop, NOT the brand's near-black.
   The dark hero background was tried first and is wrong here for one specific
   reason: this viewer exists so someone can see the color they are buying, and
   a near-black ground swallows every dark filament. Matte Charcoal on #0b0b0d
   was almost invisible — the part looked like a hole. A neutral gray reads
   correctly for dark AND light filament, which no strongly-toned backdrop can
   do, and staying neutral also avoids casting the part's color. */
.quote-viewer {
  position: relative;
  background: linear-gradient(180deg, #f4f4f7 0%, #d9d9e0 100%);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

/* A fixed aspect rather than a fixed height: the renderer reads the laid-out
   box, and a viewport-height box collapses to nothing inside a grid row that
   is sizing to its content. */
.quote-canvas {
  display: block;
  width: 100%;
  /* Wider than it is tall, and capped: at full page width a 4/3 box is most of
     a laptop screen on its own, which is what would push the price out of
     sight. 16/9 with a viewport cap keeps the part large and the number
     visible — see the note on .quote-workspace. */
  aspect-ratio: 16 / 9;
  /* 48vh, measured rather than guessed: with the heading, the compacted drop
     zone and the section padding above it, anything taller pushes the price
     off the bottom of a 900px laptop screen. It was 52vh and landed the price
     at y=901. */
  max-height: 48vh;
  /* touch-action is load-bearing on mobile — without it the browser claims the
     drag for page scrolling and the model cannot be rotated at all. */
  touch-action: none;
  cursor: grab;
}

.quote-canvas:active {
  cursor: grabbing;
}

.quote-viewer__hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: var(--sp-2);
  text-align: center;
  font-size: var(--text-xs);
  color: var(--ink-500);
  background: rgb(255 255 255 / 0.55);
  pointer-events: none;
}

/* -- Price ----------------------------------------------------------------- */

.quote-price {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid var(--line);
  min-height: 4.5rem;
  transition: opacity 0.15s;
}

/* Dimmed rather than blanked while a new price is in flight. Emptying it makes
   the number flicker on every option change, which reads as instability on the
   one element the whole page exists to show. */
.quote-price.is-loading {
  opacity: 0.45;
}

.quote-price__label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-400);
}

.quote-price__figure {
  font-family: var(--font-display);
  font-size: var(--text-4xl);
  line-height: 1.05;
  background: var(--flame-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.quote-price__unit {
  font-size: var(--text-sm);
  color: var(--ink-400);
}

/*
 * What the headline figure does NOT include.
 *
 * Sits immediately under the price rather than with the fine print at the
 * bottom of the panel, and is a readable size rather than a whisper. A total
 * that quietly grows on the payment page is drip pricing; the point of this
 * line is that nobody reaches checkout surprised, so styling it to be
 * overlooked would defeat it.
 */
.quote-price__excludes {
  margin: var(--sp-2) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-500);
}

/* -- Options --------------------------------------------------------------- */

.quote-options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--sp-4);
  padding: var(--sp-5) 0;
}

.quote-field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.quote-field > span {
  font-size: var(--text-sm);
  font-weight: 600;
}

.quote-field input,
.quote-field select {
  padding: var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--text-sm);
  background: var(--paper);
  color: var(--ink-900);
}

.quote-field input:focus-visible,
.quote-field select:focus-visible {
  outline: 2px solid var(--flame-500);
  outline-offset: 1px;
}

/* A build size the part does not fit stays visible and disabled — hiding it
   makes the option vanish mid-session, which reads as a broken control. */
.quote-field select option:disabled {
  color: var(--ink-400);
}

/* Explains an automatic size change, or a part too large for anything we run.
   Hidden via the `hidden` attribute until there is something to say. */
.quote-note {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3);
  border-left: 3px solid var(--flame-500);
  background: var(--surface-alt);
  border-radius: var(--radius);
  font-size: var(--text-sm);
  line-height: 1.45;
}

.quote-note[hidden] {
  display: none;
}

/* -- Per-body colors ------------------------------------------------------- */

.shell-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid var(--line);
}

.quote-hint {
  margin: 0 0 var(--sp-2);
  font-size: var(--text-sm);
  color: var(--ink-400);
}

.shell-row {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "swatch label" "swatch select";
  align-items: center;
  gap: var(--sp-1) var(--sp-3);
}

.shell-row__swatch {
  grid-area: swatch;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--ink-300);
}

.shell-row__label {
  grid-area: label;
  font-size: var(--text-xs);
  color: var(--ink-400);
}

.shell-row__select {
  grid-area: select;
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--text-sm);
  background: var(--paper);
}

/* -- Quantity breaks ------------------------------------------------------- */

/* Finished prices, never a discount percentage — a percentage lets anyone
   solve back to what the job costs us. */
.quote-breaks {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-5) 0;
}

.quote-break {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-4);
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink-500);
  cursor: pointer;
  text-align: left;
}

.quote-break:hover {
  border-color: var(--line-strong);
  background: var(--surface);
}

.quote-break.is-selected {
  border-color: var(--flame-500);
  background: rgb(250 139 46 / 0.07);
  color: var(--ink-900);
}

.quote-break strong {
  font-variant-numeric: tabular-nums;
}

/* -- Meta and notes -------------------------------------------------------- */

.quote-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--sp-4);
  margin: 0;
  padding: var(--sp-5) 0;
  border-top: 1px solid var(--line);
  font-size: var(--text-sm);
}

.quote-meta dt {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-400);
}

.quote-meta dd {
  margin: 2px 0 0;
  color: var(--ink-900);
}

.quote-notes ul {
  margin: 0 0 var(--sp-4);
  padding-left: var(--sp-5);
  font-size: var(--text-sm);
  color: #8a5a12;
}

.quote-disclaimer {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--ink-400);
}

/* -- Adjust the part ------------------------------------------------------- */

.quote-adjust {
  margin-top: var(--sp-4);
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface-alt);
}

.quote-adjust[hidden] {
  display: none;
}

.quote-adjust__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
  font-weight: 600;
}

.quote-adjust__size {
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--ink-400);
  font-size: var(--text-sm);
}

.quote-adjust__row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
}

.quote-adjust__label {
  min-width: 6.5rem;
  font-size: var(--text-sm);
}

.quote-adjust__buttons {
  display: flex;
  gap: var(--sp-2);
}

/* The placement actions are four wordy buttons rather than three single
   letters, so they need to wrap on a narrow panel instead of pushing the
   label off the row. */
.quote-adjust__buttons--wrap {
  flex-wrap: wrap;
}

/*
 * "Lay a face flat" is a MODE: it arms the next click on the model. A mode
 * with no visible state is one the customer cannot tell is on, and the next
 * click then does something they did not ask for.
 */
.quote-adjust__buttons .btn.is-active {
  border-color: var(--flame-500);
  background: var(--flame-100);
  color: var(--ink-900);
}

/* Short explanatory text sitting beside a control, not under it. */
.quote-adjust__hint {
  font-size: var(--text-xs);
  color: var(--ink-500);
  line-height: 1.3;
}

.quote-adjust__row input[type="number"] {
  width: 6rem;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

.btn--sm {
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--text-sm);
  min-width: 2.5rem;
}

/* Build-plate size, over the top-left of the viewer. */
.quote-viewer__plate {
  position: absolute;
  top: var(--sp-3);
  left: var(--sp-3);
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius);
  background: rgb(255 255 255 / 0.72);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-500);
  pointer-events: none;
}

.quote-adjust__row select {
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--text-sm);
  background: var(--paper);
}

.quote-adjust__row[hidden] {
  display: none;
}

/*
 * The three-field rows, below the single-column breakpoint.
 *
 * "Size mm" and "Angle °" are a 6.5rem label plus three 6rem inputs on a flex
 * row that never wrapped: 428px of content that cannot break, inside a 342px
 * column on a 390px screen. It made the whole PAGE scroll sideways — the
 * viewer, the price and the Add-to-cart button all sat 96px past the right
 * edge — and only once a model was loaded, which is why the empty page always
 * looked fine.
 *
 * Chromium found it at 390px. It predates this phase and nothing above 720px
 * shows it. Wrapping the row and giving the label its own line keeps the
 * inputs at a width a thumb can hit, rather than squeezing three fields plus a
 * label into one line.
 */
@media (max-width: 720px) {
  .quote-adjust__row {
    flex-wrap: wrap;
  }

  .quote-adjust__label {
    flex: 1 0 100%;
  }
}

/* -- Add to cart ----------------------------------------------------------- */

/*
 * Hidden in the markup until a price exists; the controller unhides it.
 *
 * The separating rule is not decoration. Everything above it happens in the
 * browser and costs nothing; pressing this button uploads the file. The note
 * under the button says so, and it stays.
 */
.quote-cart {
  margin-top: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
}

.quote-cart__note {
  margin: var(--sp-3) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-400);
}

.quote-cart__status {
  margin: var(--sp-3) 0 0;
  font-size: var(--text-sm);
}

.quote-cart__status--error {
  color: var(--danger-600);
}

.quote-cart__status--busy {
  color: var(--ink-500);
}
