/*
 * The whole stylesheet. No framework, no build step, no preprocessor: one file
 * served straight from `public/`, small enough to read in a sitting.
 *
 * The shop is meant to be understood by looking at it. Materials are swatches
 * before they are words, the cut is a shape before it is a dropdown, and the
 * configurator puts a picture of the sticker being ordered next to the controls
 * that change it. Everything that shows a material, a cut or a finish is keyed
 * on the catalog slug in a `data-slug` / `data-material` attribute, with a plain
 * neutral fallback — so a new material added in admin renders as a clean swatch
 * on day one, and giving it a texture is a rule added here and nowhere else.
 *
 * The `quote*` classes are the fragment `src/views/quote.ts` returns, including
 * its error state. They are styled here because the fragment is markup only;
 * anything it can render must have a look, or a customer's mistake arrives as
 * unstyled text.
 */

:root {
  color-scheme: light dark;

  --paper: #f6f4ef;
  --surface: #ffffff;
  --sunken: #efece5;
  --ink: #1c1b18;
  --ink-muted: #6a665e;
  --line: #e5e1d8;
  --line-strong: #cfc9bb;
  --accent: #d94f2b;
  --accent-ink: #a83a1c;
  --accent-wash: #fdf1ec;
  --danger: #a32b1e;
  --danger-wash: #fdeeeb;

  /* The always-dark band, in both schemes. A section that inverts has to stay
     inverted, or it reads as a rendering fault when the OS flips to dark. */
  --deep: #16151a;
  --deep-ink: #f4f1ea;
  --deep-muted: #a09a90;
  --deep-line: #302e36;

  --radius: 12px;
  --radius-lg: 20px;
  --shadow: 0 1px 2px rgb(28 27 24 / 6%), 0 8px 24px rgb(28 27 24 / 5%);
  --shadow-lift: 0 2px 6px rgb(28 27 24 / 8%), 0 18px 48px rgb(28 27 24 / 10%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #16151a;
    --surface: #1f1e24;
    --sunken: #191820;
    --ink: #f2efe9;
    --ink-muted: #a29d95;
    --line: #2e2c35;
    --line-strong: #4a4753;
    --accent: #ff7a55;
    --accent-ink: #ff9878;
    --accent-wash: #2b211f;
    --danger: #ff8a7a;
    --danger-wash: #2e1c1a;
    --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 8px 24px rgb(0 0 0 / 30%);
    --shadow-lift: 0 2px 6px rgb(0 0 0 / 45%), 0 18px 48px rgb(0 0 0 / 40%);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Several controls here are revealed by script — the preset chips, a tile whose
   finish the chosen material cannot take. `hidden` is a UA `display: none`, and
   any layout rule below would quietly outrank it and put a dead control back on
   the page. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 16px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
  -webkit-text-size-adjust: 100%;
}

h1,
h2,
h3,
p,
fieldset,
legend {
  margin: 0;
}

fieldset {
  border: 0;
  padding: 0;
  min-width: 0;
}

a {
  color: var(--accent-ink);
}

/* Present to a screen reader, absent from the page. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ================================================================= chrome == */

.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(10px);
  padding: 0.85rem 1.25rem;
}

.site-header__mark {
  font-weight: 800;
  font-size: 1rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
}

.site-header__mark::after {
  content: "";
  display: inline-block;
  width: 0.42rem;
  height: 0.42rem;
  margin-left: 0.35rem;
  border-radius: 50%;
  background: var(--accent);
  vertical-align: 0.05em;
}

/* The nav is a second slim bar under the mark, and scrolls sideways on a phone
   rather than wrapping to two lines and pushing the page down. */
.site-nav {
  display: flex;
  gap: 1.4rem;
  max-width: 74rem;
  margin: 0 auto;
  padding: 0.7rem 1.25rem;
  overflow-x: auto;
  scrollbar-width: none;
}

.site-nav::-webkit-scrollbar {
  display: none;
}

.site-nav__link {
  color: var(--ink-muted);
  text-decoration: none;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  padding: 0.15rem 0;
  border-bottom: 2px solid transparent;
}

.site-nav__link:hover {
  color: var(--ink);
}

.site-nav__link[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.site-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1.25rem;
  max-width: 74rem;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 2.5rem;
  border-top: 1px solid var(--line);
  font-size: 0.84rem;
}

.site-footer__link {
  color: var(--ink-muted);
}

.site-footer__note {
  color: var(--ink-muted);
  margin-left: auto;
}

/* ================================================================ swatches == */
/*
 * The visual stand-in for one catalog row, drawn entirely in CSS. `data-slug`
 * carries the slug; the default below is what an unknown one gets, so a new
 * material is a clean neutral chip rather than an empty box.
 */

.swatch {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  border-radius: 10px;
  border: 1px solid var(--line-strong);
  background: linear-gradient(140deg, var(--sunken), var(--surface));
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 45%);
}

/* --- materials ------------------------------------------------------------ */

.swatch--material[data-slug="white-vinyl"] {
  background: linear-gradient(140deg, #ffffff 0%, #f2f2f0 55%, #e6e6e2 100%);
  border-color: #d8d6cf;
}

/* Transparent stock, shown the way every graphics program shows transparency,
   with a glassy sheen over it. */
.swatch--material[data-slug="clear-vinyl"] {
  background:
    linear-gradient(115deg, rgb(255 255 255 / 75%) 0%, rgb(255 255 255 / 10%) 42%, rgb(255 255 255 / 60%) 100%),
    repeating-conic-gradient(#e9e9e6 0% 25%, #fbfbfa 0% 50%) 0 0 / 14px 14px;
  border-color: #d8d6cf;
}

.swatch--material[data-slug="holographic"] {
  background: conic-gradient(
    from 210deg,
    #ff8fbf,
    #ffd36e,
    #8affc1,
    #6ec6ff,
    #c58cff,
    #ff8fbf
  );
  border-color: #cfc9bb;
}

.swatch--material[data-slug="matte-paper"] {
  background:
    repeating-linear-gradient(48deg, rgb(0 0 0 / 3.5%) 0 1px, transparent 1px 3px),
    linear-gradient(140deg, #faf7f0, #ece7db);
  border-color: #d8d2c4;
}

/* --- cuts ----------------------------------------------------------------- */
/*
 * A cut is a shape, so the swatch draws one: the sticker in white on the stock,
 * with the cut path dashed around it. The three differ in exactly the way the
 * cuts do — an outline that follows the art, an outline on a square backing,
 * and no outline at all.
 */

.swatch--cut,
.swatch--finish {
  aspect-ratio: 1 / 1;
  width: 2.6rem;
  position: relative;
  background: linear-gradient(140deg, #ffffff, #eeece7);
  border-color: var(--line-strong);
  overflow: hidden;
}

.swatch--cut::before {
  content: "";
  position: absolute;
  inset: 22%;
  border: 1.5px dashed var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.swatch--cut[data-slug="die-cut"]::before {
  inset: 18%;
  border-radius: 58% 42% 48% 52% / 52% 46% 54% 48%;
}

.swatch--cut[data-slug="kiss-cut"]::before {
  inset: 26%;
  border-radius: 30%;
}

.swatch--cut[data-slug="kiss-cut"]::after {
  content: "";
  position: absolute;
  inset: 14%;
  border: 1.5px solid var(--line-strong);
  border-radius: 4px;
}

.swatch--cut[data-slug="sheet"]::before {
  inset: 16%;
  border-style: solid;
  border-color: var(--line-strong);
  border-radius: 3px;
  background: repeating-conic-gradient(
      color-mix(in srgb, var(--accent) 16%, transparent) 0% 25%,
      transparent 0% 50%
    )
    0 0 / 50% 50%;
}

/* --- finishes ------------------------------------------------------------- */

.swatch--finish[data-slug="gloss"] {
  background: linear-gradient(125deg, #ffffff 0%, #dfe6ec 38%, #ffffff 52%, #cdd6de 70%, #ffffff 100%);
}

.swatch--finish[data-slug="matte"] {
  background: linear-gradient(140deg, #efece6, #ddd8cf);
}

.swatch--finish[data-slug="none"] {
  background:
    linear-gradient(45deg, transparent calc(50% - 1px), var(--line-strong) 50%, transparent calc(50% + 1px)),
    linear-gradient(140deg, #ffffff, #f3f1ec);
}

/* ================================================================== home == */

.hero {
  max-width: 74rem;
  margin: 0 auto;
  padding: 3.5rem 1.25rem 3rem;
  display: grid;
  gap: 2.5rem;
  align-items: center;
}

@media (min-width: 58rem) {
  .hero {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    padding: 5rem 1.25rem 4rem;
  }
}

.eyebrow {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-ink);
  margin-bottom: 1rem;
}

.hero__title {
  font-size: clamp(2.4rem, 1.4rem + 4.4vw, 4.2rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  font-weight: 800;
}

.hero__lede {
  margin-top: 1.1rem;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.15rem);
  color: var(--ink-muted);
  max-width: 30rem;
}

.hero .cta {
  margin: 1.75rem 0 0;
}

.hero .button {
  font-size: 1.02rem;
  padding: 0.85rem 1.7rem;
}

/* The from-price rides under the button as a quiet line, not a panel: it is a
   reassurance on the way to the configurator, not a pricing table. */
.price-from {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  max-width: 26rem;
}

.price-from__figure {
  font-size: 1.15rem;
  letter-spacing: -0.01em;
}

.price-from__figure strong {
  font-weight: 800;
  font-size: 1.5rem;
  letter-spacing: -0.03em;
}

.price-from__detail {
  color: var(--ink-muted);
  font-size: 0.85rem;
  margin-top: 0.3rem;
}

/* --- the three stickers in the hero --------------------------------------- */

.hero__art {
  position: relative;
  min-height: 17rem;
  display: grid;
  place-items: center;
}

.hero__art .chip {
  position: absolute;
  border-radius: 26% 74% 33% 67% / 62% 30% 70% 38%;
  box-shadow: var(--shadow-lift);
  border: 0.5rem solid var(--surface);
}

.chip--a {
  width: 12.5rem;
  height: 12.5rem;
  background: conic-gradient(from 200deg, #ff8fbf, #ffd36e, #8affc1, #6ec6ff, #c58cff, #ff8fbf);
  transform: rotate(-8deg);
  z-index: 2;
}

.chip--b {
  width: 8.5rem;
  height: 8.5rem;
  background: linear-gradient(145deg, var(--accent), #f4a26b);
  transform: translate(-6rem, -4rem) rotate(14deg);
  border-radius: 62% 38% 47% 53% / 40% 56% 44% 60%;
}

.chip--c {
  width: 7rem;
  height: 7rem;
  background: linear-gradient(145deg, #ffffff, #e4e0d6);
  transform: translate(5.6rem, 4.2rem) rotate(-19deg);
  border-radius: 48% 52% 63% 37% / 55% 42% 58% 45%;
}

@media (prefers-reduced-motion: no-preference) {
  .chip--a {
    animation: float 9s ease-in-out infinite;
  }

  .chip--b {
    animation: float 11s ease-in-out infinite reverse;
  }
}

/* Scaled down on a phone so the outermost sticker does not hang off the edge. */
@media (max-width: 46rem) {
  .hero__art {
    min-height: 15rem;
  }

  .chip--a {
    width: 11rem;
    height: 11rem;
  }

  .chip--b {
    width: 7.5rem;
    height: 7.5rem;
    transform: translate(-5rem, -3.4rem) rotate(14deg);
  }

  .chip--c {
    width: 6rem;
    height: 6rem;
    transform: translate(4.8rem, 3.6rem) rotate(-19deg);
  }
}

@keyframes float {
  0%,
  100% {
    translate: 0 0;
  }

  50% {
    translate: 0 -0.6rem;
  }
}

/* --- bands ---------------------------------------------------------------- */

.band {
  padding: 3.5rem 0;
  border-top: 1px solid var(--line);
}

.band__inner {
  max-width: 74rem;
  margin: 0 auto;
  padding: 0 1.25rem;
}

.band__title {
  font-size: clamp(1.5rem, 1.2rem + 1.3vw, 2.1rem);
  letter-spacing: -0.025em;
  font-weight: 800;
  margin-bottom: 1.75rem;
}

.band__more {
  margin-top: 1.5rem;
  font-size: 0.9rem;
}

.band--dark {
  background: var(--deep);
  color: var(--deep-ink);
  border-top-color: var(--deep);
}

.band--dark a {
  color: #ffb59b;
}

.band--cta {
  text-align: center;
}

.band--cta .cta {
  display: flex;
  justify-content: center;
}

/* --- material cards ------------------------------------------------------- */

.cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}

.card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 0.75rem 0.75rem 1.1rem;
  box-shadow: var(--shadow);
}

.card .swatch {
  aspect-ratio: 16 / 10;
  border-radius: 14px;
  margin-bottom: 0.9rem;
}

.card__title {
  margin: 0 0 0.3rem;
  font-size: 1.02rem;
  padding: 0 0.35rem;
}

.card__body {
  font-size: 0.86rem;
  color: var(--ink-muted);
  margin: 0;
  padding: 0 0.35rem;
}

/* --- how it works --------------------------------------------------------- */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1.75rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}

.step__n {
  display: grid;
  place-items: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  border: 1px solid currentcolor;
  color: var(--accent);
  font-size: 0.85rem;
  font-weight: 800;
  margin-bottom: 0.8rem;
}

.step__title {
  font-size: 1.05rem;
  letter-spacing: -0.01em;
  margin-bottom: 0.3rem;
}

.step__body {
  font-size: 0.9rem;
  color: var(--deep-muted);
}

/* ================================================================ studio == */
/*
 * The configurator. Two columns on a wide screen: the preview on the left,
 * sticky, and the choices on the right. One column on a phone, preview first,
 * because the first thing a customer wants is to see the thing.
 */

.studio {
  max-width: 74rem;
  margin: 0 auto;
  /* Bottom padding clears the sticky price bar, which is out of flow. */
  padding: 1.75rem 1.25rem 10rem;
}

.studio__grid {
  display: grid;
  gap: 1.75rem;
  align-items: start;
}

@media (min-width: 62rem) {
  .studio__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    gap: 3rem;
  }
}

.studio__options {
  display: grid;
  gap: 1.25rem;
  align-content: start;
  min-width: 0;
}

/* --- the preview ---------------------------------------------------------- */

/* The preview is a card, exactly like the option groups facing it. It was a
   bare label over a flat panel, which put a whisper at the top of one column
   and a bold card header at the top of the other — the two halves read as
   different pages. Same surface, same border, same header height, same top. */
.stage {
  min-width: 0;
  gap: 0.9rem;
}

@media (min-width: 62rem) {
  .stage {
    position: sticky;
    top: 6.5rem;
  }

}

/* Where the option cards carry a step number, the preview carries a mark of the
   same size and weight, so both columns open on a header of identical height. */
.opt__eye {
  flex: none;
  width: 1.55rem;
  height: 1.55rem;
  border-radius: 46% 54% 52% 48% / 54% 48% 52% 46%;
  background: linear-gradient(140deg, #ffffff, #e6e3db);
  border: 1px solid var(--line-strong);
}

/* A quiet dot grid behind the sticker, so a white sticker on white stock is
   still visibly a sticker. */
.stage__frame {
  /* Letterboxed on a phone, where a square frame would be most of the screen
     and push every control below the fold. */
  position: relative;
  aspect-ratio: 4 / 3;
  display: grid;
  place-items: center;
  padding: 2rem;
  border-radius: 14px;
  background:
    radial-gradient(circle, var(--line) 1px, transparent 1px) 0 0 / 18px 18px,
    var(--sunken);
  border: 1px solid var(--line);
}

/* After the base rule, not inside the `.stage` block above: a media query adds
   no specificity, so an override has to come later in the file to win. */
@media (min-width: 62rem) {
  .stage__frame {
    aspect-ratio: 1 / 1;
  }
}

.stage__sticker {
  position: relative;
  width: 50%;
  aspect-ratio: 1 / 1;
  border-radius: 18%;
  background: linear-gradient(140deg, #ffffff 0%, #f2f2f0 55%, #e6e6e2 100%);
  box-shadow: var(--shadow-lift);
  transition:
    width 180ms ease,
    aspect-ratio 180ms ease;
}

/* The cut path, drawn where the blade would run. */
.stage__sticker::before {
  content: "";
  position: absolute;
  inset: 6%;
  border: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  border-radius: inherit;
  pointer-events: none;
}

.stage__art {
  position: absolute;
  inset: 13%;
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
}

.stage__empty {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 1rem;
  text-align: center;
  font-size: 0.8rem;
  color: #8a857c;
}

/* --- what the cut does to the shape --------------------------------------- */

.stage__sticker[data-cut="kiss-cut"] {
  border-radius: 8px;
}

.stage__sticker[data-cut="kiss-cut"]::before {
  inset: 12%;
  border-radius: 22%;
}

.stage__sticker[data-cut="kiss-cut"] .stage__art {
  inset: 20%;
}

.stage__sticker[data-cut="sheet"] {
  border-radius: 6px;
}

.stage__sticker[data-cut="sheet"]::before {
  inset: 5%;
  border-radius: 3px;
}

/* --- what the material does to the surface -------------------------------- */

.stage__sticker[data-material="clear-vinyl"] {
  background:
    linear-gradient(115deg, rgb(255 255 255 / 70%) 0%, rgb(255 255 255 / 8%) 42%, rgb(255 255 255 / 55%) 100%),
    repeating-conic-gradient(#e9e9e6 0% 25%, #fbfbfa 0% 50%) 0 0 / 18px 18px;
}

.stage__sticker[data-material="holographic"] {
  background: conic-gradient(
    from 210deg,
    #ff8fbf,
    #ffd36e,
    #8affc1,
    #6ec6ff,
    #c58cff,
    #ff8fbf
  );
}

.stage__sticker[data-material="matte-paper"] {
  background:
    repeating-linear-gradient(48deg, rgb(0 0 0 / 3.5%) 0 1px, transparent 1px 3px),
    linear-gradient(140deg, #faf7f0, #ece7db);
}

/* --- what the finish does to the light ------------------------------------ */

/* A gloss laminate is a specular streak. Matte and none get nothing, which is
   the honest difference between them. */
.stage__sticker[data-finish="gloss"]::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  /* Kept light on purpose: this streak sits on top of the customer's artwork,
     and at any real strength it washes the colours out — which is exactly the
     thing they are looking at the preview to judge. */
  background: linear-gradient(
    118deg,
    rgb(255 255 255 / 0%) 32%,
    rgb(255 255 255 / 26%) 44%,
    rgb(255 255 255 / 0%) 56%
  );
}

.stage__caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.75rem;
}

.stage__size {
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.stage__spec {
  font-size: 0.85rem;
  color: var(--ink-muted);
}

.stage__note {
  margin-top: -0.5rem;
  font-size: 0.78rem;
  color: var(--ink-muted);
  max-width: 28rem;
}

/* ================================================== the option groups == */

.config {
  display: grid;
  gap: 1.25rem;
}

.opt {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 1.35rem;
  box-shadow: var(--shadow);
  display: grid;
  gap: 0.9rem;
  align-content: start;
}

.opt__title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  padding: 0;
}

.opt__n {
  display: grid;
  place-items: center;
  flex: none;
  width: 1.55rem;
  height: 1.55rem;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-size: 0.78rem;
  font-weight: 800;
}

/* --- tiles ---------------------------------------------------------------- */

/* Two across, not three: the catalog's names are sentences — "Die-Cut (cut to
   shape)", "Kiss-Cut (on backing)" — and a third column turns every one of them
   into four wrapped lines. */
.tiles {
  display: grid;
  gap: 0.6rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}

.tile {
  display: grid;
  gap: 0.5rem;
  align-content: start;
  justify-items: start;
  text-align: left;
  padding: 0.7rem;
  border: 1.5px solid var(--line);
  border-radius: 14px;
  background: var(--sunken);
  cursor: pointer;
  font: inherit;
  color: inherit;
  position: relative;
}

.tile:hover {
  border-color: var(--line-strong);
}

.tile__input {
  /* The tile is the control; the radio is there for the form and the
     accessibility tree. Clipped rather than `display: none` so it keeps focus
     and stays announced. */
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.tile:has(.tile__input:checked),
.tile.is-on {
  border-color: var(--accent);
  background: var(--accent-wash);
}

.tile:has(.tile__input:focus-visible),
.tile:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.tile__name {
  font-weight: 700;
  font-size: 0.92rem;
  line-height: 1.25;
}

.tile__note {
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--ink-muted);
}

/* Cut and finish tiles put their square swatch beside the name, not above it:
   the swatch is a small icon there, where a material's is the whole point. */
.tile--cut,
.tile--finish {
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0.6rem;
}

/* --- preset chips --------------------------------------------------------- */

.presets {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
}

.chip-btn {
  font: inherit;
  font-weight: 700;
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
  color: inherit;
  background: var(--sunken);
  border: 1.5px solid var(--line);
  border-radius: 999px;
  padding: 0.4rem 0.95rem;
  cursor: pointer;
}

.chip-btn:hover {
  border-color: var(--line-strong);
}

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

.chip-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- plain fields --------------------------------------------------------- */

.field {
  display: grid;
  gap: 0.35rem;
  align-content: start;
  min-width: 0;
}

.field__label {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
  padding: 0;
}

.field__hint {
  font-size: 0.78rem;
  color: var(--ink-muted);
}

.field-row {
  display: grid;
  gap: 0.9rem;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
}

.control {
  width: 100%;
  font: inherit;
  color: inherit;
  background: var(--surface);
  border: 1.5px solid var(--line-strong);
  border-radius: 10px;
  padding: 0.6rem 0.7rem;
  min-height: 2.85rem;
}

select.control {
  /* Keep the native control: it is the one widget a phone renders better than
     anything hand-built. */
  appearance: auto;
}

/* The select stayed in the DOM and stayed the control; `configurator.js` built
   tiles from its options and this stops it being drawn twice. Without that
   script the rule never applies and the select is what the customer uses. */
.control--upgraded {
  display: none;
}

.control:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* `:user-invalid`, not `:invalid`.
   `:invalid` matches an empty `required` field the moment the page loads, so
   every form opened with a red email box telling the customer they had made a
   mistake before they had typed anything. `:user-invalid` waits until they have
   actually interacted with the field or tried to submit. */
.control:user-invalid {
  border-color: var(--danger);
  background: var(--danger-wash);
}

.control[aria-invalid="true"] {
  border-color: var(--danger);
  background: var(--danger-wash);
}

/* ============================================================== the drop == */

.drop {
  display: grid;
  justify-items: center;
  gap: 0.35rem;
  padding: 2rem 1.25rem;
  border: 2px dashed var(--line-strong);
  border-radius: 16px;
  background: var(--sunken);
  cursor: pointer;
  text-align: center;
  transition:
    border-color 120ms ease,
    background 120ms ease;
}

.drop:hover,
.drop.is-over,
.drop:focus-within {
  border-color: var(--accent);
  background: var(--accent-wash);
}

.drop__input {
  /* Visually gone, still the control: clicking the label opens the picker with
     no script involved, and the input keeps its place in the tab order. */
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

/* An upward arrow out of a tray, drawn in two boxes. */
.drop__icon {
  width: 2.4rem;
  height: 2.4rem;
  margin-bottom: 0.35rem;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  position: relative;
}

.drop__icon::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0.62rem;
  width: 0.62rem;
  height: 0.62rem;
  border-left: 2px solid var(--accent);
  border-top: 2px solid var(--accent);
  transform: translateX(-50%) rotate(45deg);
}

.drop__icon::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0.72rem;
  width: 2px;
  height: 0.95rem;
  background: var(--accent);
  transform: translateX(-50%);
}

.drop__lead {
  font-weight: 700;
}

.drop__link {
  color: var(--accent-ink);
  text-decoration: underline;
}

.drop__hint {
  font-size: 0.8rem;
  color: var(--ink-muted);
}

/* ============================================================ price bar == */
/*
 * The price follows the customer down the page. It is fixed rather than sticky
 * because the two columns above it are different heights, and a sticky element
 * inside either one leaves the bar behind on the short side.
 */

.pricebar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 15;
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(12px);
  padding: 0.85rem 1.25rem 1rem;
  box-shadow: 0 -8px 32px rgb(28 27 24 / 8%);
}

.pricebar__inner {
  max-width: 74rem;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
}

.pricebar__note {
  max-width: 74rem;
  margin: 0.4rem auto 0;
  font-size: 0.72rem;
  color: var(--ink-muted);
}

.pricebar .button {
  flex: none;
}

/* A request is in flight: dim the old price rather than blanking it, so the bar
   does not collapse and reflow the page under the customer's thumb. */
.pricebar.htmx-request .quote {
  opacity: 0.45;
}

/* On a phone the bar is competing with the page for a small screen, so it keeps
   the two numbers a customer is watching — the total and the unit price — and
   drops the production detail. Nothing money-related is hidden: the setup fee
   and the order minimum stay, because a total that silently includes them is
   the one thing here worth complaining about. */
@media (max-width: 46rem) {
  .pricebar {
    padding: 0.6rem 1rem 0.7rem;
  }

  .pricebar__inner {
    gap: 0.5rem;
  }

  .pricebar__note,
  .quote__layout {
    display: none;
  }

  .quote {
    flex-basis: 100%;
    gap: 0 0.5rem;
  }

  .quote__total {
    font-size: 1.3rem;
    white-space: nowrap;
  }

  .quote__unit {
    font-size: 0.82rem;
    white-space: nowrap;
  }

  .quote__setup,
  .quote__minimum {
    font-size: 0.72rem;
  }

  /* Full width on its own line. Squeezed onto the price's line it is either a
     button too small to hit or a price wrapped onto three lines. */
  .pricebar .button {
    flex: 1;
    padding: 0.65rem 1rem;
    font-size: 0.95rem;
    min-height: 2.75rem;
  }

  .studio {
    padding-bottom: 7rem;
  }
}

/* ================================================================= price == */

.quote {
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem 0.8rem;
  transition: opacity 120ms ease-in;
}

.quote__total {
  font-size: 1.7rem;
  line-height: 1.1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.quote__total strong {
  font-weight: 800;
}

.quote__unit {
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.quote__setup,
.quote__minimum,
.quote__layout {
  font-size: 0.8rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  flex-basis: 100%;
}

.quote__pending {
  color: var(--ink-muted);
}

/* The error state replaces the price in place, so it has to read as the price
   having something to say rather than as a different component. */
.quote--error {
  background: var(--danger-wash);
  border-left: 3px solid var(--danger);
  border-radius: 6px;
  padding: 0.6rem 0.8rem;
}

.quote__error {
  color: var(--danger);
  font-size: 0.92rem;
}

/* ================================================================ button == */
/*
 * Worn by the upload form's `<button>`, the checkout submits and the content
 * pages' call-to-action `<a>`, so it carries the properties an anchor needs — a
 * link is inline and underlined by default and a button is neither. One
 * definition, because two would drift into two different-looking buttons on the
 * same site.
 */

.button {
  justify-self: start;
  display: inline-block;
  text-decoration: none;
  text-align: center;
  font: inherit;
  font-weight: 700;
  color: #fff;
  background: var(--accent);
  border: 1.5px solid var(--accent);
  border-radius: 10px;
  padding: 0.7rem 1.4rem;
  min-height: 2.85rem;
  cursor: pointer;
}

.button:hover:not(:disabled) {
  background: var(--accent-ink);
  border-color: var(--accent-ink);
}

.button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Disabled is the upload button's resting state until a file is chosen AND
   Turnstile has produced a token, so it has to read as "not yet", not "broken". */
.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The secondary action inside a card, where a filled button would outrank the
   page's real call to action. */
.button--quiet {
  background: transparent;
  color: var(--accent-ink);
  border-color: var(--line-strong);
}

.button--quiet:hover:not(:disabled) {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.cta {
  margin: 1.5rem 0;
}

/* ================================================================ upload == */

.upload__lede {
  color: var(--ink-muted);
  font-size: 0.88rem;
  max-width: 40rem;
}

.upload__body {
  display: grid;
  gap: 0.9rem;
}

/* The no-JavaScript and no-sitekey states. Plain, not alarming: nothing is
   broken from the customer's side, there is just another way to send the file. */
.upload__notice {
  border-left: 3px solid var(--line-strong);
  border-radius: 6px;
  background: var(--sunken);
  padding: 0.75rem 0.9rem;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.upload__notice--error {
  border-left-color: var(--danger);
  background: var(--danger-wash);
  color: var(--danger);
}

.upload__file {
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}

/* What the file says about its own size, and the arithmetic that got there. */
.upload__suggest {
  font-size: 0.85rem;
  color: var(--ink-muted);
  border-left: 3px solid var(--accent);
  background: var(--accent-wash);
  border-radius: 6px;
  padding: 0.6rem 0.8rem;
}

/* A button that does something small and local, so it should look like the link
   it replaces rather than compete with the page's real call to action. */
.linkish {
  font: inherit;
  font-weight: 700;
  color: var(--accent-ink);
  background: none;
  border: 0;
  padding: 0;
  text-decoration: underline;
  cursor: pointer;
}

.linkish:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The single size field and the size it produces. */
.size-custom {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.control--short {
  width: 6rem;
  flex: none;
}

.size-result {
  font-size: 0.85rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* An escape hatch, so it is quiet: almost nobody wants a size that is not the
   shape of the artwork they just sent us. */
.size-toggle {
  font-size: 0.8rem;
}

.size-toggle .linkish {
  color: var(--ink-muted);
  font-weight: 600;
}

/* The one-line explanation under an option group's heading. */
.opt__lede {
  font-size: 0.85rem;
  color: var(--ink-muted);
  margin-top: -0.35rem;
}

.upload__progress {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.upload__bar {
  flex: 1;
  min-width: 0;
  height: 0.6rem;
  appearance: none;
  border: 0;
  border-radius: 999px;
  background: var(--line);
}

.upload__bar::-webkit-progress-bar {
  border-radius: 999px;
  background: var(--line);
}

.upload__bar::-webkit-progress-value {
  border-radius: 999px;
  background: var(--accent);
}

.upload__bar::-moz-progress-bar {
  border-radius: 999px;
  background: var(--accent);
}

.upload__percent {
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  min-width: 3ch;
  text-align: right;
}

.upload__status:empty {
  display: none;
}

.upload__status {
  display: grid;
  gap: 0.5rem;
  border-radius: 8px;
  font-size: 0.9rem;
}

.upload__status--busy {
  color: var(--ink-muted);
}

/* A hard reject: wrong type, empty, oversize, size mismatch, or a failed
   verification. These block, so they look like the price's error. */
.upload__status--error {
  background: var(--danger-wash);
  border-left: 3px solid var(--danger);
  padding: 0.75rem 0.9rem;
  color: var(--danger);
}

/* A success, with or without advisories. Deliberately NOT the error styling:
   preflight warnings never block an order, and dressing them in red would send
   customers away from a job the shop was happy to print. */
.upload__status--ok {
  background: var(--accent-wash);
  border-left: 3px solid var(--accent);
  padding: 0.75rem 0.9rem;
}

.upload__received {
  font-weight: 700;
}

.upload__advisory-lede {
  color: var(--ink-muted);
}

.upload__advisories {
  margin: 0;
  padding-left: 1.1rem;
  display: grid;
  gap: 0.35rem;
  color: var(--ink-muted);
}

/* ================================================================ content == */
/*
 * Everything below dresses the storefront's content pages — materials, pricing,
 * artwork guidelines, the FAQ and the legal drafts. They share one shell
 * (`src/views/pages/shell.ts`), so they share one set of rules here rather than
 * each inventing its own.
 *
 * Measure is capped on paragraphs and lists only, never on the containers, so a
 * table or a card grid still uses the full column while the prose stays
 * readable.
 */

.page {
  max-width: 68rem;
  margin: 0 auto;
  padding: 2.5rem 1.25rem 4rem;
}

.page__title {
  font-size: clamp(1.9rem, 1.3rem + 2.2vw, 2.8rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
  font-weight: 800;
}

.page__lede {
  margin-top: 0.6rem;
  color: var(--ink-muted);
  max-width: 36rem;
  font-size: 1.02rem;
}

.page__body {
  margin-top: 2rem;
}

/* `:where()` zeroes the selector's specificity, so a later single-class rule
   like `.card__body` overrides the measure and the margin by ordinary cascade
   order instead of by `!important`. */
.page__body :where(p) {
  margin: 0 0 0.9rem;
  max-width: 42rem;
}

.page__body :where(p:last-child) {
  margin-bottom: 0;
}

/* ------------------------------------------------------- blocks and lists -- */

.block {
  margin-top: 2.75rem;
}

.block__title {
  font-size: 1.3rem;
  letter-spacing: -0.02em;
  font-weight: 700;
  margin-bottom: 0.9rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--line);
}

.qa {
  margin-bottom: 1.4rem;
}

.qa__q {
  margin: 0 0 0.35rem;
  font-size: 1rem;
}

.links {
  margin: 0 0 0.9rem;
  padding-left: 1.2rem;
  max-width: 42rem;
}

.links li {
  margin-bottom: 0.45rem;
}

/* A yes-list and a no-list are read at a glance, so mark them at a glance. */
.links--yes,
.links--no {
  list-style: none;
  padding-left: 0;
}

.links--yes li,
.links--no li {
  padding-left: 1.6rem;
  position: relative;
}

.links--yes li::before,
.links--no li::before {
  position: absolute;
  left: 0;
  font-weight: 700;
}

.links--yes li::before {
  content: "\2713";
  color: var(--accent-ink);
}

.links--no li::before {
  content: "\2717";
  color: var(--ink-muted);
}

/* ----------------------------------------------------------------- notices -- */

.notice {
  border: 1px solid var(--line-strong);
  border-left-width: 3px;
  border-radius: 8px;
  padding: 0.9rem 1rem;
  margin-bottom: 1.5rem;
  background: var(--surface);
}

.notice--good {
  border-left-color: var(--accent);
  background: var(--accent-wash);
}

.notice--draft,
.notice--closed {
  border-left-color: var(--danger);
  background: var(--danger-wash);
}

/* An unfinished sentence, shown to the owner in the page rather than hidden in
   a code comment. It is meant to be conspicuous and short-lived. */
.todo {
  display: inline-block;
  background: var(--danger-wash);
  color: var(--danger);
  border: 1px dashed var(--danger);
  border-radius: 5px;
  padding: 0.05rem 0.4rem;
  font-size: 0.88rem;
}

.todo strong {
  letter-spacing: 0.08em;
  font-size: 0.75rem;
  text-transform: uppercase;
}

/* -------------------------------------------------------- material listing -- */

.listings {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}

.listing {
  display: grid;
  grid-template-columns: 5rem 1fr;
  gap: 1rem;
  align-items: start;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 1.1rem;
  box-shadow: var(--shadow);
}

/* Out of stock is dimmed, not hidden: a customer who came for this material
   needs to see that we carry it and that it is coming back. */
.listing--out {
  background: transparent;
  border-style: dashed;
  box-shadow: none;
}

.listing--out .swatch {
  filter: saturate(0.35);
  opacity: 0.7;
}

.listing__text {
  min-width: 0;
}

.listing__title {
  margin: 0 0 0.35rem;
  font-size: 1.05rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.listing__body {
  margin: 0 0 0.35rem;
}

.listing__body--muted {
  color: var(--ink-muted);
  font-size: 0.88rem;
}

.listing__meta {
  margin: 0;
  font-size: 0.82rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.badge {
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-radius: 999px;
  padding: 0.15rem 0.55rem;
  border: 1px solid currentcolor;
  white-space: nowrap;
}

.badge--in,
.badge--good {
  color: var(--accent-ink);
  background: var(--accent-wash);
}

.badge--ok {
  color: var(--ink-muted);
}

.badge--out,
.badge--warn {
  color: var(--danger);
  background: var(--danger-wash);
}

/* ------------------------------------------------------------------ tables -- */

/* The wrapper scrolls, not the page: a four-column price table is wider than a
   phone, and a body that scrolls sideways feels broken. */
.table-scroll {
  overflow-x: auto;
  margin-bottom: 0.75rem;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  min-width: 26rem;
}

.table caption {
  caption-side: bottom;
  text-align: left;
  padding-top: 0.7rem;
  font-size: 0.82rem;
  color: var(--ink-muted);
}

.table th,
.table td {
  text-align: left;
  padding: 0.6rem 0.9rem 0.6rem 0;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.table thead th {
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-muted);
  border-bottom-color: var(--line-strong);
}

.table tbody th {
  font-weight: 600;
  white-space: nowrap;
}

.table__formats {
  font-weight: 600;
  white-space: nowrap;
}

/* Marks the cell where the order minimum, not the arithmetic, set the total. */
.table__flag {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
  white-space: nowrap;
}

.table-note {
  font-size: 0.85rem;
  color: var(--ink-muted);
}

/* =============================================================== checkout == */
/*
 * Checkout is deliberately narrower than the rest of the shop — one column of
 * decisions, no marketing beside it.
 */

.checkout {
  max-width: 34rem;
  margin: 0 auto;
  padding: 2.5rem 1.25rem 4rem;
}

.checkout h1 {
  font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin: 0 0 0.35rem;
}

.checkout__form {
  display: grid;
  gap: 1.1rem;
  margin-top: 1.5rem;
}

.checkout__form .button {
  justify-self: start;
  margin-top: 0.25rem;
}

/* Order summary shown alongside the address and rate steps. */
.checkout__subtotal,
.checkout__email,
.checkout__address {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  margin: 0 0 1rem;
}

.checkout__address {
  display: grid;
  gap: 0.15rem;
  line-height: 1.45;
}

.checkout__country {
  color: var(--ink-muted);
}

.checkout__edit {
  font-size: 0.9rem;
}

/* --- shipping rate picker ------------------------------------------------- */

.rate {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 0.25rem 0.7rem;
  background: var(--surface);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  cursor: pointer;
}

.rate:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-wash);
}

.rate:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.rate__name {
  font-weight: 600;
}

.rate__price {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  text-align: right;
}

.rate__eta,
.rate__total {
  grid-column: 2 / -1;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.rate__total {
  font-variant-numeric: tabular-nums;
}

/* --- validation ----------------------------------------------------------- */

.form-errors {
  background: var(--danger-wash);
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  padding: 0.8rem 1rem;
  margin: 0 0 1.25rem;
  color: var(--danger);
}

.form-errors ul {
  margin: 0;
  padding-left: 1.1rem;
}

.error {
  color: var(--danger);
  font-size: 0.9rem;
}
