/* Hallmark · pre-emit critique: P5 H5 E5 S5 R4 V4 */
/* Hallmark · macrostructure: Split Studio (App family) · genre: editorial
 * design-system: design.md · designed-as-app
 *
 * Shared by /tags/[slug], /personalised/[slug] and /cart. That sharing is the
 * point rather than an accident: before this pass each of those routes was
 * dressed separately and they read as three different shops. This file is the
 * floor all three stand on — page stylesheets dress it from outside and never
 * fork it.
 *
 * Every class name below is API. The e2e suite drives `.stage`, `.price`,
 * `.field`, `.field-error`, `.tag-canvas`, `.gen-canvas` and the cart line
 * classes by name. Restyle them freely; do not rename them.
 */

/* ---- The bench ------------------------------------------------------------
   A diptych: the object on the left, the controls on the right. The stage is
   the only framed thing on the page — the controls sit directly on the paper
   behind a hairline, because two equally-framed panels read as two equivalent
   things and the denser one wins. The object is the product. */

.configurator {
  display: grid;
  gap: var(--space-2xl);
  grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  align-items: start;
}

.stage {
  /* `sticky` already establishes a containing block for absolutely-positioned
     descendants, so `.stage-controls` anchors to the stage on wide screens.
     The narrow-screen rule below switches this to `static`, which would NOT —
     so it sets `position: relative` there instead of unsetting positioning
     altogether. */
  position: sticky;
  top: var(--space-md);
  aspect-ratio: 4 / 3;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  /* Was `linear-gradient(165deg, #eef1ee, #dfe4e2)` — a cool grey-green
     sitting inside a warm bone page. The most-looked-at box on the site was
     fighting its own background. Same bone, one step down in lightness, with
     a single inset highlight for the sense that the object sits slightly
     below the page rather than floating above it. A card floating above a
     card is what makes a configurator look like a form. */
  background: var(--paper-2);
  box-shadow: inset 0 1px 0 oklch(100% 0 0 / 0.7);
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* The one place motion carries information rather than decorating: the stage
   settles as the geometry resolves, so the entrance is the signal that the
   object finished building. */
.stage {
  animation: stage-settle var(--dur-slow) var(--ease-out) both;
}

@keyframes stage-settle {
  from { opacity: 0; transform: scale(0.985); }
  to { opacity: 1; transform: none; }
}

/* `.gen-canvas` is the personalised products' preview (GeneratedCanvas.tsx).
   Same rules, listed together rather than duplicated so the two previews
   cannot drift into behaving differently under the pointer. */
.tag-canvas,
.gen-canvas {
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
  cursor: grab;
}

.tag-canvas:active,
.gen-canvas:active {
  cursor: grabbing;
}

/* ---- Rotate controls ------------------------------------------------------
   WCAG 2.2 AA §2.5.7 Dragging Movements: any author-controlled drag operation
   needs a single-pointer alternative, and keyboard operation has to be
   retained. The preview was drag-only — `role="img"` on a canvas with pointer
   handlers in the scene module and no tabIndex, so a keyboard or switch user
   could never turn the object they were about to buy.

   These buttons are that alternative. They sit over the stage rather than
   below it so they read as belonging to the object, and they stay visible
   rather than appearing on hover: a hover-only affordance is no affordance at
   all on a touch screen. */

.stage-controls {
  position: absolute;
  right: var(--space-sm);
  bottom: var(--space-sm);
  display: flex;
  gap: var(--space-2xs);
  z-index: var(--z-raised);
}

.stage-rotate {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-3);
  color: var(--ink-3);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.stage-rotate:hover {
  border-color: var(--ink-3);
  color: var(--ink);
}

.stage-rotate svg {
  width: 18px;
  height: 18px;
  display: block;
}

.stage-note {
  color: var(--ink-3);
  padding: var(--space-lg);
  text-align: center;
  margin: 0;
}

.stage-error {
  color: var(--danger);
  max-width: 34ch;
}

/* ---- Controls -------------------------------------------------------------
   Deliberately not a card. It was one: a raised, bordered panel beside a
   stage with the identical border and radius. */

.controls {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

/* `.field` is a <fieldset>: zero out the browser's default border, padding and
   margin, and `min-width: 0` so it cannot overflow its flex/grid container —
   fieldsets default to `min-width: min-content`, which forces the whole
   `.controls` column wider than its parent on narrow screens. */
.field {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

/* Labels sit on ink-2 rather than the lightest grey: they are the only way
   through the form, and a column of faint labels has no hierarchy at all. */
.field-label {
  padding: 0;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}

.field-hint {
  font-size: var(--text-sm);
  color: var(--ink-3);
}

/* Validation for the field it sits inside. Same red as `.stage-error`, so an
   error reads as an error wherever it appears, at control-label size rather
   than the stage's larger centred note. */
.field-error {
  font-size: var(--text-sm);
  color: var(--danger);
}

/* Not an error — reaching the character limit is normal — so it reads as
   emphasis on the ink colour rather than as red. */
.field-notice {
  font-size: var(--text-sm);
  color: var(--ink);
}

/* The name is the product, so its input is the one control set at display
   size. Every other choice on the page is secondary to typing it.
   16px minimum: anything smaller makes iOS Safari zoom on focus. */
.text-input {
  min-height: 44px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
  font-family: inherit;
  font-size: var(--text-base);
  background: var(--paper-3);
  color: var(--ink);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.text-input:hover:not(:disabled) {
  border-color: var(--ink-3);
}

.text-input.qty {
  width: 96px;
}

.text-input:disabled {
  background: var(--paper-2);
  color: var(--ink-3);
  cursor: not-allowed;
  opacity: 0.6;
}

.controls-note {
  font-size: var(--text-sm);
  color: var(--ink-3);
  margin: 0;
}

/* ---- Chips and swatches ---------------------------------------------------
   Both are the full 44px touch target. Chips reach it with min-height;
   swatches keep a 32px disc and extend the hit area past their visual bounds
   with a pseudo-element, because a 44px circle of flat colour would shout
   louder than the object it describes. */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.chip {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-md);
  background: var(--paper-3);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  color: var(--ink);
  /* Clickable text must never wrap: it reads as a layout fault and makes the
     tap target's bounds ambiguous. */
  white-space: nowrap;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.chip:hover:not(:disabled):not(.chip-on) {
  border-color: var(--ink);
}

.chip-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.swatch {
  position: relative;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 2px solid transparent;
  box-shadow: 0 0 0 1px var(--line-strong);
  cursor: pointer;
  transition:
    transform var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

/* The hit area, not the disc. 44×44 centred on a 32px circle, which is what
   makes these tappable without making them loud. */
.swatch::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
}

.swatch:hover:not(:disabled) {
  transform: translateY(-1px);
}

/* Selection is carried by the ring, which is also what a keyboard user sees,
   so the state is never hover-only. */
.swatch-on {
  border-color: var(--paper);
  box-shadow: 0 0 0 2px var(--ink);
}

.swatch:disabled {
  opacity: 0.32;
  cursor: not-allowed;
}

.swatch:disabled:hover {
  transform: none;
}

/* ---- Price and buy --------------------------------------------------------
   The summary sits above a hairline so the money is visibly the end of the
   column rather than one more field in it. */

.summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding-top: var(--space-md);
  border-top: 1px solid var(--line);
}

.price {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.price-note {
  font-size: var(--text-sm);
  color: var(--ink-3);
}

.cta {
  min-height: 44px;
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-lg);
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.cta:hover:not(:disabled) {
  background: var(--accent-deep);
}

.cta:active:not(:disabled) {
  transform: translateY(1px);
}

/* Disabled carries opacity as well as colour: colour alone is not a reliable
   signal for a user who cannot distinguish the two greys. */
.cta:disabled {
  background: var(--ink-3);
  opacity: 0.45;
  cursor: not-allowed;
}

.cta-note {
  font-size: var(--text-sm);
  color: var(--ink-3);
  margin: 0;
}

@media (max-width: 54rem) {
  .configurator {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xl);
  }

  /* A sticky stage on a short viewport pins the preview on top of the
     controls the customer is reaching for. */
  .stage {
    position: relative;
    aspect-ratio: 3 / 2;
  }
}

/* ---- Cart · the Ledger family ---------------------------------------------
   A list with a total, not a diptych. Typography only, tabular figures
   throughout, no imagery beyond each line's own small flat preview. */

.cart-empty {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  align-items: flex-start;
}

.cart-lines {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.cart-line {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  gap: var(--space-md);
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--paper-3);
  animation: cart-line-in var(--dur-base) var(--ease-out) both;
}

@keyframes cart-line-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* Small SVG preview, not the configurator's 3D stage — a flat rendering of the
   actual shape, colours and text so lines with similar colours (or identical
   colours and different names) stay visually distinguishable. */
.cart-thumb {
  width: 56px;
  height: 42px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--paper-2);
}

.cart-thumb-svg,
.cart-thumb-svg svg {
  display: block;
  width: 100%;
  height: 100%;
}

.cart-thumb-placeholder {
  width: 100%;
  height: 100%;
  background: repeating-linear-gradient(
    45deg,
    var(--paper-2) 0,
    var(--paper-2) 3px,
    var(--line) 3px,
    var(--line) 6px
  );
}

.cart-line-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
}

.cart-line-sub {
  font-size: var(--text-sm);
  color: var(--ink-3);
}

.cart-qty {
  width: 64px;
  min-height: 44px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  padding: var(--space-xs) var(--space-sm);
  font-family: inherit;
  font-size: var(--text-base);
  background: var(--paper-3);
  color: var(--ink);
}

.cart-remove {
  min-height: 44px;
  border: 0;
  background: none;
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.18em;
  transition: color var(--dur-fast) var(--ease-out);
}

.cart-remove:hover {
  color: var(--danger);
}

.cart-error {
  color: var(--danger);
}

/* Sold-out colour on one line. Same red as the other two error surfaces, at
   line-detail size, so it reads as a problem with this row rather than with
   the cart as a whole — and on its own tinted surface so it is findable
   without relying on colour alone. */
.cart-line-error {
  grid-column: 1 / -1;
  margin: 0;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--danger-soft);
  font-size: var(--text-sm);
  color: var(--danger);
}

.cart-line-price {
  font-size: var(--text-sm);
  color: var(--ink-3);
}

/* Two columns, labels left and amounts right-aligned, so the figures line up
   as a column the eye can add. */
.cart-totals {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2xs) var(--space-md);
  margin: var(--space-lg) 0 var(--space-xs);
  max-width: 320px;
}

.cart-totals dt {
  color: var(--ink-3);
  font-size: var(--text-sm);
}

.cart-totals dd {
  margin: 0;
  text-align: right;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

.cart-total-label,
.cart-totals dd.cart-total-value {
  font-weight: 600;
  color: var(--ink);
  font-size: var(--text-base);
}

.cart-note {
  font-size: var(--text-sm);
  color: var(--ink-3);
}

@media (max-width: 40rem) {
  /* The quantity and remove controls drop under the line's body rather than
     squeezing four columns into 320px. */
  .cart-line {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .cart-qty,
  .cart-remove {
    grid-column: 2;
    justify-self: start;
  }
}

@media (prefers-reduced-motion: reduce) {
  .stage,
  .cart-line {
    animation: none;
  }

  .swatch:hover:not(:disabled),
  .cta:active:not(:disabled) {
    transform: none;
  }
}

/* The postage band beside the amount. Secondary to the figure: the number is
   what the customer is deciding on, the band name is why it is that number. */
.cart-ship-band {
  color: var(--ink-3);
  font-size: var(--text-xs);
  white-space: nowrap;
}
