/* SA-22: the shared Smart Art components, for the V2 design.
 *
 * V2 must NOT load smartart.css: its global selectors (body, html, h1-h3,
 * a, :root, an !important on inputs) are V1's design and would fight this
 * one. That is the same rule SA-4 wrote for the admin Marketing Studio.
 *
 * What IS shared is the machinery: the compositor stage that preview.js
 * drives, the artwork popup and the lightbox. Those are component-scoped
 * already, so they are copied here rather than reimplemented -- the JS
 * behind them (homography.js, preview.js, canvasstage.js, designstyle.js,
 * quote.js, site.js) is used unchanged by both designs.
 *
 * KEEP IN STEP with smartart.css if the visualiser markup changes, exactly
 * as studio_preview.css must be.
 */

/* SA-22: V1 tokens this extract still refers to, mapped to the V2 palette.
 *
 * An undefined custom property makes the WHOLE declaration invalid, so it
 * silently falls back to whatever was inherited -- that is how the active
 * filter chip ended up ink on ink and unreadable. Anything copied out of
 * smartart.css has to bring its variables with it.
 */
:root {
  --bone-2: var(--linen);
  --ink-soft: var(--muted);
  --rule: 1px solid var(--line);
  --white: #fff;
}

/* studio_preview.css — the visualiser *component* only, for the admin Marketing
 * Studio. This is a deliberate subset of smartart.css: it carries the .preview
 * stage/layer/handle/bar/roomstrip rules but NONE of the global selectors
 * (body, html, h1-h3, a, :root, input !important) that would otherwise wreck the
 * admin chrome. Required CSS vars are scoped to .preview so nothing leaks. Keep
 * the .preview rules in step with smartart.css if the visualiser markup changes.
 */
.preview {
  --ink: #1b1a16;
  --bone: #efe9dd;
  --brass: #b8924e;
  --brass-lit: #d8b878;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  position: relative; background: var(--ink); overflow: hidden; border-radius: 8px;
}
.preview .stagebox { position: relative; width: 100%; aspect-ratio: 3 / 2; overflow: hidden; }
.preview .stage { position: absolute; left: 50%; top: 50%; transform-origin: center; will-change: transform; }
.preview .layer { position: absolute; inset: 0; width: 100%; height: 100%; }
.preview .pattern {
  position: absolute; top: 0; left: 0; transform-origin: 0 0;
  background-repeat: no-repeat; background-size: 100% 100%;
  mix-blend-mode: multiply; pointer-events: none;
}
.preview img.shade { mix-blend-mode: multiply; }
.preview img.base, .preview img.fg, .preview img.shade { pointer-events: none; }

.preview .edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; }
.preview .edges polygon { fill: none; stroke: var(--brass-lit); stroke-width: 3; stroke-dasharray: 10 8; vector-effect: non-scaling-stroke; }
.preview.adjust .edges { opacity: 1; }

.preview .handles { position: absolute; inset: 0; pointer-events: none; }
.preview.adjust .handles { pointer-events: auto; }
.preview .handle {
  position: absolute; width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border-radius: 50%; border: 2px solid var(--brass-lit); background: rgba(27,26,22,0.55);
  cursor: grab; opacity: 0; transition: opacity 0.2s; touch-action: none;
}
.preview.adjust .handle { opacity: 1; }
.preview .handle:active { cursor: grabbing; background: var(--brass); }

.preview .bar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 16px; background: #14140f; border-top: 1px solid #2b2921; flex-wrap: wrap;
}
.preview .bar label { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: #8d8779; }
.preview .roomsel { display: flex; align-items: center; gap: 12px; }
.preview select {
  background: #201f18; color: var(--bone); border: 1px solid #3a382e; border-radius: 2px;
  padding: 9px 12px; font-family: var(--sans); font-size: 14px; min-width: 190px;
}
.preview .toggle {
  background: transparent; border: 1px solid #3a382e; color: #b3ada0; cursor: pointer;
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  padding: 10px 16px; border-radius: 2px; font-family: var(--sans);
}
.preview .toggle:hover { color: var(--bone); border-color: #555245; }
.preview .toggle[aria-pressed="true"] { background: var(--brass); border-color: var(--brass); color: #14140f; }

.preview .loading { position: absolute; inset: 0; display: grid; place-items: center; color: #6f6a5e; font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase; }

.preview .roomstrip {
  display: flex; gap: 8px; overflow-x: auto; padding: 10px 12px;
  background: #1b1a16; border-top: 1px solid #302e27; scrollbar-width: thin;
}
.preview .roomstrip button {
  flex: 0 0 auto; width: 84px; padding: 0; border: 2px solid transparent;
  background: none; cursor: pointer; border-radius: 2px; overflow: hidden; line-height: 0;
}
.preview .roomstrip button img { width: 100%; height: 56px; object-fit: cover; }
.preview .roomstrip button span {
  display: block; font-size: 10px; line-height: 1.3; padding: 4px 2px;
  color: #8d8779; font-family: var(--sans); text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.preview .roomstrip button:hover { border-color: #55524533; }
.preview .roomstrip button[aria-pressed="true"] { border-color: var(--brass); }
.preview .roomstrip button[aria-pressed="true"] span { color: var(--bone); }

/* SA-10 chips. Kept in step with smartart.css by hand, per the note at the top
 * of this file: the studio must never load smartart.css, whose global selectors
 * bleed into the admin chrome. */
.preview .roomcats {
  display: flex; gap: 6px; overflow-x: auto; padding: 10px 12px 0;
  background: #1b1a16; border-top: 1px solid #302e27; scrollbar-width: thin;
}
.preview .roomcats + .roomstrip { border-top: 0; padding-top: 8px; }
.preview .roomcats button {
  flex: 0 0 auto; padding: 6px 12px; border: 1px solid #3a382f; background: none;
  color: #8d8779; font-family: var(--sans); font-size: 11px; letter-spacing: .04em;
  text-transform: uppercase; border-radius: 999px; cursor: pointer; white-space: nowrap;
}
.preview .roomcats button:hover { border-color: #555245; color: var(--bone); }
.preview .roomcats button[aria-pressed="true"] {
  border-color: var(--brass); color: var(--bone); background: #2a2721;
}
.preview .roomstrip button[hidden] { display: none; }

/* SA-11: a round vinyl sticker is not a rectangle. The radius is applied in the
 * pattern layer's OWN unwarped space and the matrix3d then carries it onto the
 * quad, so it lands as the ellipse inscribed in that quad. Without it the print
 * would be painted across the sticker's square corners and onto the wall. */
.preview .pattern.ellipse { border-radius: 50%; }

/* Touch targets on coarse pointers (matches smartart.css SA-2). */
@media (pointer: coarse) {
  .preview .handle { width: 44px; height: 44px; margin: -22px 0 0 -22px; border-width: 3px; }
  .preview .toggle, .preview select { min-height: 44px; }
}

/* ==================== SA-18: the artwork popup ==================== */
/* Full screen on a phone, a centred panel on a desktop. The picture gets the
   room, the words sit beside it, and the controls stay OFF the image -- the
   SA-5 lesson, where arrows over the photo covered the thing being looked at. */
.artpop { position: fixed; inset: 0; z-index: 200; display: flex;
          align-items: center; justify-content: center; }
.artpop[hidden] { display: none; }
.artpop-scrim { position: absolute; inset: 0; background: rgba(20,20,20,.82); }
/* Stop the page behind from scrolling under the popup. */
body.artpop-open { overflow: hidden; }
.artpop-panel {
  position: relative; z-index: 1; background: var(--white);
  width: min(1180px, 94vw); max-height: 92vh; overflow: auto;
  display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(280px, 1fr);
  box-shadow: 0 30px 90px rgba(0,0,0,.4);
}
.artpop-art { background: var(--bone-2); display: flex; align-items: center;
              justify-content: center; min-height: 0; }
.artpop-art img { display: block; width: 100%; height: 100%;
                  max-height: 92vh; object-fit: cover; }
.artpop-side { padding: 40px 36px; display: flex; flex-direction: column; }
.artpop-coll { margin: 0; font-family: var(--sans); font-size: 11.5px;
               letter-spacing: .18em; text-transform: uppercase; color: var(--brass); }
.artpop-name { font-size: clamp(24px, 2.4vw, 34px); margin: 10px 0 14px; color: var(--ink); }
.artpop-desc { margin: 0 0 26px; color: var(--muted); font-size: 15px; line-height: 1.7; }
.artpop-acts { display: flex; flex-direction: column; gap: 12px; align-items: flex-start;
               margin-top: auto; }
.artpop-acts .btn { width: 100%; }
.artpop-count { margin: 22px 0 0; font-family: var(--sans); font-size: 11.5px;
                letter-spacing: .12em; text-transform: uppercase; color: var(--line); }
.artpop-x {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.92); border: 1px solid var(--line);
  border-radius: 50%; cursor: pointer; color: var(--ink);
}
.artpop-x:hover { background: var(--white); }
.artpop-nav { position: absolute; left: 0; bottom: 0; display: flex; gap: 1px; z-index: 2; }
.artpop-nav button {
  width: 52px; height: 46px; border: 0; cursor: pointer;
  background: rgba(255,255,255,.92); color: var(--ink); font-size: 17px;
  border-top: 1px solid var(--line); border-right: 1px solid var(--line);
}
.artpop-nav button:hover { background: var(--brass); color: #fff; }
@media (max-width: 900px) {
  /* Full screen on a phone: a 94vw panel inside a scrim wastes the only screen
     dimension that matters, and the design is the point of the page. */
  .artpop-panel {
    width: 100vw; height: 100dvh; max-height: none;
    grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto;
  }
  .artpop-art { min-height: 0; }
  .artpop-art img { max-height: none; }
  .artpop-side { padding: 22px 20px 26px; }
  .artpop-name { margin: 8px 0 10px; }
  .artpop-desc { margin-bottom: 18px; max-height: 5.5em; overflow: hidden; }
  .artpop-acts { flex-direction: row; flex-wrap: wrap; gap: 10px; }
  .artpop-acts .btn { width: auto; flex: 1 1 46%; min-height: 48px; }
  .artpop-count { display: none; }
  /* Close LEFT, arrows RIGHT. Both were pinned to the top right, so the close
     control sat underneath the "next" arrow and the only reliable way out of a
     full-screen popup was the browser's back button. */
  .artpop-x { top: 10px; left: 10px; right: auto; }
  .artpop-nav { left: auto; right: 10px; bottom: auto; top: 10px; gap: 8px; }
  .artpop-nav button {
    width: 44px; height: 44px; border-radius: 50%;
    border: 1px solid var(--line);
  }
}
/* Visually hidden, but present: the field needs a label for a screen reader and
   the placeholder is not one. */
.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
/* Lightbox */
.pf-lb {
  position: fixed; inset: 0; background: rgba(10,9,8,0.94); display: none;
  align-items: center; justify-content: center; z-index: 200; padding: 3vh 3vw;
}
.pf-lb[open], .pf-lb.on { display: flex; }
.pf-lb img { max-width: 100%; max-height: 94vh; object-fit: contain; border-radius: 2px; }
.pf-lb .x, .pf-lb .nav {
  position: absolute; background: rgba(255,255,255,0.1); color: var(--white); border: 0;
  width: 48px; height: 48px; border-radius: 50%; cursor: pointer; font-size: 20px; line-height: 1;
}
.pf-lb .x { top: 18px; right: 18px; }
.pf-lb .nav.prev { left: 18px; top: 50%; transform: translateY(-50%); }
.pf-lb .nav.next { right: 18px; top: 50%; transform: translateY(-50%); }
.pf-lb .x:hover, .pf-lb .nav:hover { background: rgba(255,255,255,0.22); }
/* The lightbox caption (SA-6: artwork titles). Sits under the photo on every
   width, above the arrow bar on a phone. */
.pf-lb .cap {
  color: var(--bone); font-family: var(--serif); font-size: 17px;
  margin: 16px 0 0; text-align: center; max-width: 60ch;
}

/* SA-22: the room strip is ~2300px wide and a grid/flex item's automatic
   minimum size is its content's min-content width, so any column holding the
   compositor refuses to shrink and the PAGE scrolls sideways. Measured at
   2584px of overflow on the design page. This is the SA-2 fix, restated for
   the V2 wrappers. */
.product-detail > *, .product-gallery, .vis-wrap > *, .container > .preview {
  min-width: 0;
}
.preview .roomstrip { max-width: 100%; }


/* SA-22: the tap-target floor for the shared compositor controls. V1 gets this
   from the SA-2 block in smartart.css, which is not part of this extract, so
   the room chips, the room strip, the style toggles and the corner handles were
   all under 44px in V2. A tablet is a touch device too, so the rule is keyed on
   max-width, not on a phone breakpoint. */
@media (max-width: 1000px) {
  .preview .roomcats button,
  .preview .roomstrip button,
  .preview .toggle,
  .preview .bar button,
  .preview select,
  .vis-tabs button,
  .fp-presets button,
  #stylebar button,
  .sb-seg button,
  .stylebar button,
  .vis-colls a,
  .vis-colls button,
  .vis-all { min-height: 44px; display: inline-flex; align-items: center; }
  .preview .handle { width: 44px; height: 44px; }
  /* iOS zooms the page when a focused control is under 16px. */
  .preview select, .preview input { font-size: 16px; }
}


/* The shared range chips are a navigation list at every width, so they take the
   24px AA floor on a pointer too, not only the 44px touch floor above. */
.vis-colls a, .vis-colls button, .vis-all {
  min-height: 32px; display: inline-flex; align-items: center;
}

/* LAST ON PURPOSE: see the note at the foot of style.css. The base floor above
   is the same specificity, so the touch floor has to be declared after it. */
@media (max-width: 1000px) {
  .vis-colls a, .vis-colls button, .vis-all { min-height: 44px; }
}

/* The range picker is a <select>, not a link: "Boho Patterns" was the selected
   option's text and the control itself was under the floor. Selectors matter
   more than intent here -- .vis-colls a never matched it. */
.vis-colls select { min-height: 32px; }
@media (max-width: 1000px) {
  .vis-colls select { min-height: 44px; font-size: 16px; }
}

/* ── SA-24: the design picker ──────────────────────────────────────────────────
 * `.vis-swatches` was the one part of the visualiser machinery that did not come
 * across into this extract, so V2 had NONE of the phone treatment SA-2 and SA-7
 * built for it: no 44px floor (the swatches measured 31px), no height cap, and
 * no sideways scroller, which on a phone puts a thousand swatches in a column
 * under the wall and scrolls the only thing that matters off the screen.
 *
 * Written in V2's OWN tokens rather than copied with V1's. An undefined custom
 * property invalidates the whole declaration, which is the trap already
 * recorded at the top of this file.
 */
.vis-swatches { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
@media (max-width: 900px) { .vis-swatches { grid-template-columns: repeat(4, 1fr); } }
.vis-swatches button {
  padding: 0; border: 2px solid var(--line); background: none; cursor: pointer;
  line-height: 0; border-radius: var(--radius-sm); overflow: hidden;
}
.vis-swatches button img { width: 100%; aspect-ratio: 1; object-fit: cover; }
.vis-swatches button span {
  display: block; font-size: 11px; padding: 5px 4px; color: var(--muted);
  font-family: var(--sans); line-height: 1.3; text-align: center;
}
.vis-swatches button[aria-pressed="true"] { border-color: var(--sage); }
.vis-swatches button[aria-pressed="true"] span { color: var(--ink); }

/* The picture must stay on screen while they browse, or the picker is useless. */
body[data-page="my-room"] .vis-swatches,
body[data-page="visualiser"] .vis-swatches {
  max-height: min(62vh, 620px); overflow-y: auto; padding-right: 4px;
}

@media (max-width: 900px) {
  /* Two rows that scroll sideways instead of a mile of column: the wall stays
     put while you thumb through the catalogue, which is how this is used --
     standing in the room. */
  body[data-page="my-room"] .vis-swatches,
  body[data-page="visualiser"] .vis-swatches {
    max-height: none;
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-template-rows: repeat(2, auto);
    grid-auto-columns: 104px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    padding: 0 0 8px;
    -webkit-overflow-scrolling: touch;
  }
  body[data-page="my-room"] .vis-swatches button,
  body[data-page="visualiser"] .vis-swatches button { scroll-snap-align: start; }
}

/* The touch floor is declared LAST on purpose. SA-22b: a base rule appended
   after a 44px media query silently undoes it at equal specificity, on every
   phone. */
@media (max-width: 1000px) {
  .vis-swatches button, .preview .roomstrip button,
  .preview .roomcats button, .preview .toggle, .preview select { min-height: 44px; }
}

/* ── SA-26: the shared visualiser and my-room CHROME ───────────────────────────
 * SA-22b brought the compositor stage across and stopped there, so everything
 * AROUND it was still V1 markup with no V2 styling: the tool had no container
 * at all and ran edge to edge, and every control -- the artwork and colour
 * toggles, the range picker, the stock search -- rendered as raw browser
 * defaults against the left edge of the viewport.
 *
 * `.shell` is V1's width container and `.section.tight` its band; V2 calls
 * these `.container` and `.section-tight`. Rather than rewrite a shared partial
 * that BOTH designs include (and risk V1), the two names are given V2
 * definitions here. Written in V2's own tokens, never V1's, because an
 * undefined custom property invalidates the whole declaration -- the trap
 * recorded at the top of this file.
 */
.shell { width: min(1220px, calc(100% - 40px)); margin-inline: auto; }
.section.tight { padding: 38px 0; }

.section-head { max-width: 620px; margin-bottom: 34px; }
.section-head h2 { font-family: var(--serif); font-size: clamp(1.7rem, 3.2vw, 2.5rem); margin: 10px 0 12px; }
.section-head p { color: var(--muted); margin: 0; }

/* The artwork / colour controls under the stage. */
.stylebar {
  display: grid; gap: 10px; margin: 16px 0 4px; padding: 14px 16px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card);
}
.sb-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.sb-row input[type="range"] { flex: 1; min-width: 120px; accent-color: var(--sage); }
.sb-row output { font-size: .8rem; color: var(--ink); min-width: 2ch; text-align: right; }
.sb-label {
  font-size: .69rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); font-family: var(--sans); min-width: 62px;
}
.sb-seg { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.sb-seg button {
  padding: 9px 15px; cursor: pointer; font: inherit; font-size: .78rem;
  font-family: var(--sans); color: var(--muted); background: none;
  border: 0; border-right: 1px solid var(--line);
}
.sb-seg button:last-child { border-right: 0; }
/* `--sage-dark`, not `--sage`: white on the lighter sage measures 4.27, and a
   pressed control is a resting state somebody has to read. */
.sb-seg button.on { background: var(--sage-dark); color: #fff; }

/* Our designs / Search stock. */
.vis-tabs { display: flex; gap: 4px; margin-bottom: 14px; }
.vis-tabs button {
  flex: 1; padding: 11px 6px; white-space: nowrap; cursor: pointer; font: inherit;
  font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); background: none; border: 0;
  border-bottom: 2px solid var(--line);
}
.vis-tabs button.on { color: var(--ink); border-bottom-color: var(--sage); }

.vis-colls { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.vis-colls select {
  flex: 1 1 auto; min-width: 0; padding: 10px 12px; font-family: var(--sans);
  font-size: .82rem; color: var(--ink); background: #fff;
  border: 1px solid var(--line); border-radius: 999px;
}
.vis-colls .vis-all {
  flex: 0 0 auto; font-size: .7rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); text-decoration: none;
}
.vis-colls .vis-all:hover { color: var(--sage-dark); }

.hint { font-size: .78rem; color: var(--muted); }
.empty-note { color: var(--muted); padding: 40px 0; }

/* The Freepik stock search. */
.fp-search { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.fp-search input {
  width: 100%; min-width: 0; padding: 11px 13px; border: 1px solid var(--line);
  border-radius: 999px; font: inherit; font-size: .87rem; background: #fff;
}
.fp-search input:focus { outline: 2px solid var(--sage); outline-offset: 1px; }
.fp-search .btn { width: 100%; }
.fp-note { font-size: .78rem; color: var(--muted); margin: 0 0 12px; line-height: 1.5; }
.fp-presets {
  display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px;
  max-height: 116px; overflow-y: auto; overscroll-behavior: contain;
}
.fp-presets button {
  padding: 7px 12px; cursor: pointer; font: inherit; font-size: .72rem;
  font-family: var(--sans); color: var(--muted); background: none;
  border: 1px solid var(--line); border-radius: 999px;
}
.fp-presets button:hover { color: var(--ink); border-color: var(--ink); }

/* The two-column tool. `min-width: 0` is load bearing: a grid item's automatic
   minimum is its content's min-content width, and the room strip is ~2,300px
   wide, so without it the page scrolls sideways (SA-2). */
.vis-wrap { display: grid; grid-template-columns: 1fr 240px; gap: 28px; align-items: start; }
.vis-wrap > * { min-width: 0; }
.vis-wrap .preview { width: min(100%, calc(min(56vh, 520px) * 1.5)); margin-inline: auto; }
.vis-wrap .stagebox { aspect-ratio: 3 / 2; }
.vis-wrap .actions { display: flex; flex-direction: column; gap: 10px; }

@media (max-width: 900px) {
  .vis-wrap { grid-template-columns: 1fr; }
  .vis-wrap .preview { width: 100%; }
  .fp-presets { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; }
  .sb-label { min-width: 100%; }
}

/* Declared last: a base rule appended after a touch media query silently undoes
   it at equal specificity (SA-22b). */
@media (max-width: 1000px) {
  .sb-seg button, .vis-tabs button, .fp-presets button,
  .vis-colls select, .vis-colls .vis-all { min-height: 44px; }
  .fp-search input { min-height: 44px; font-size: 16px; }
}

/* ── SA-26: "See it on your own wall" ─────────────────────────────────────────
 * Same story as the visualiser chrome: the page is a shared V1 partial whose
 * styling never came across, so the upload panel, the tips and every status
 * message rendered unstyled. V2 tokens throughout; the semantic colours for
 * error and warning are their own, because a red or amber that has to read as
 * a warning is not the brand accent.
 */
.wall-upload { max-width: 720px; margin: 0 auto 48px; }
.wall-drop {
  border: 2px dashed var(--line); border-radius: var(--radius);
  background: var(--linen); padding: 52px 28px; text-align: center;
  transition: border-color .2s, background .2s;
}
.wall-drop.over { border-color: var(--sage); background: #fff; }
.wall-drop h3 { font-family: var(--serif); font-size: 1.45rem; margin: 16px 0 6px; }
.wall-icon { width: 54px; height: 54px; color: var(--sage-dark); }
.wall-hint { color: var(--muted); font-size: .85rem; margin: 0; }
.wall-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 24px; }
.wall-privacy {
  color: var(--muted); font-size: .8rem; margin: 30px 0 0;
  padding-top: 20px; border-top: 1px solid var(--line);
}
.wall-tips {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px;
  border-top: 1px solid var(--line); padding-top: 32px;
}
.wall-tips h4 { font-family: var(--serif); font-size: 1.05rem; margin: 0 0 8px; }
.wall-tips p { color: var(--muted); font-size: .88rem; margin: 0; }

.wall-busy { text-align: center; padding: 60px 20px; }
.wall-busy p { margin: 0 0 6px; }
.spin {
  width: 34px; height: 34px; margin: 0 auto 20px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--sage);
  animation: wall-spin .9s linear infinite;
}
@keyframes wall-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }

.wall-error {
  margin: 20px 0 0; padding: 14px 16px; border-radius: var(--radius-sm);
  background: #fdf2f0; border: 1px solid #e6c4bd; color: #8a3122; font-size: .88rem;
}
.wall-warn {
  margin: 14px 0 0; padding: 13px 16px; border-radius: var(--radius-sm);
  font-size: .88rem; background: #fdf7ec; border: 1px solid #e3d0a8; color: #6b5322;
}
.wall-warn a { color: #6b5322; text-decoration: underline; white-space: nowrap; }

.wall-bar { margin-top: 20px; max-width: 400px; }
.wall-bar-row { display: flex; gap: 10px; margin-top: 10px; }
.wall-bar-row .btn { flex: 1 1 0; text-align: center; }
.wall-coll {
  width: 100%; margin-bottom: 14px; padding: 11px 13px; font-family: var(--sans);
  font-size: .87rem; color: var(--ink); background: #fff;
  border: 1px solid var(--line); border-radius: 999px;
}
.wall-status { font-size: 1rem; }
.wall-status.check::before {
  content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 9px; vertical-align: .12em; background: var(--muted);
}
.wall-status.ok::before { background: var(--sage); }
.wall-saved {
  margin: 12px 0 0; font-size: .72rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--sage-dark);
}
.only-touch { display: none; }
@media (pointer: coarse) { .only-touch { display: block; } }

@media (max-width: 900px) {
  .wall-tips { grid-template-columns: 1fr; gap: 20px; }
  .wall-drop { padding: 40px 20px; }
  .wall-bar, .wall-bar-row .btn { max-width: none; }
}
@media (max-width: 1000px) {
  .wall-coll { min-height: 44px; font-size: 16px; }
}

/* SA-26: the repeats slider measured 16px tall. A range input draws its own
   track and thumb, so growing the ELEMENT box gives it a real hit area without
   thickening the track: the browser centres the track in whatever height it is
   given. Declared after the touch block above, deliberately. */
.sb-row input[type="range"] { height: 32px; }
@media (max-width: 1000px) {
  .sb-row input[type="range"] { height: 44px; }
}

/* ── SA-26: the design card ────────────────────────────────────────────────────
 * `.tile` and `.art` are the artwork-popup contract (SA-18) and come from the
 * shared grid markup, so V2 was rendering them with no rule at all: each card's
 * image took its own natural height, the cards ended at different points, and
 * the short ones carried a band of dead space. Cards in a row are one object --
 * same edges, same baselines -- and an aspect ratio is what makes that true.
 */
.tile { display: flex; flex-direction: column; }
.tile .art {
  position: relative; display: block; overflow: hidden;
  aspect-ratio: 4 / 3; background: var(--linen);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.tile .art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.tile .meta { padding: 14px 16px 0; }
.tile .meta h3 { font-family: var(--serif); font-size: 1.15rem; margin: 0 0 2px; }
.tile .meta h3 a { color: var(--ink); text-decoration: none; }
.tile .meta h3 a:hover { color: var(--sage-dark); }
.tile .acts { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
              padding: 12px 16px 16px; margin-top: auto; }

/* The newsletter's reply, which is the only thing on the page after a sign-up. */
.signup-done {
  margin: 12px 0 0; padding: 11px 16px; border-radius: var(--radius-sm);
  background: var(--sage-soft); color: var(--ink); font-size: .87rem;
}

/* ══ SA-28: the visualiser, in her colours ═══════════════════════════════════
 * The compositor chrome was still V1's: `.preview` re-scoped a near-black and a
 * brass gold, so the tool sat in the linen page as a dark block from the other
 * design. It is retheme-able through four tokens because every rule inside the
 * component reads them, plus the handful of hardcoded darks below.
 *
 * THE ORANGE IS HER LOGO'S, MEASURED OFF THE FILE: #ffa038. It is 1.80 as text
 * on her paper and 2.03 under a white label, so it is never either of those.
 * Dark ink ON it measures 7.31, so it is a FILL that carries ink -- which is
 * how the logo uses it too, as a solid bar rather than as type.
 *
 * Selection is orange throughout the TOOL (room, category, swatch, toggle) and
 * stays sage on the browsing pages. One accent per surface: the visualiser is
 * an instrument, and "this is the one you picked" should not be the same colour
 * as "this is the page you are on".
 */
.preview {
  --ink: #272823;          /* the stage backdrop: V2's own warm near-black */
  --bone: #272823;         /* was text on dark; the chrome is light now */
  --brass: #ffa038;        /* her logo orange */
  --brass-lit: #ffa038;
  background: var(--linen);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
/* The picture keeps a deep ground so letterboxing reads as deliberate. */
.preview .stagebox { background: #272823; }

/* The bar under the picture, and the two strips, join the page. */
.preview .bar,
.preview .roomstrip,
.preview .roomcats {
  background: var(--paper);
  border-top: 1px solid var(--line);
}
.preview .bar label { color: var(--muted); }
.preview select,
.preview .toggle {
  background: #fff; color: var(--ink);
  border: 1px solid var(--line); border-radius: 999px;
}
.preview .toggle:hover { color: var(--ink); border-color: var(--sage); }
/* Ink on the orange, never white: 7.31 against 2.03. */
.preview .toggle[aria-pressed="true"] {
  background: var(--brass); border-color: var(--brass); color: #272823;
}

.preview .roomstrip button { border-color: transparent; }
.preview .roomstrip button:hover { border-color: var(--line); }
.preview .roomstrip button span { color: var(--muted); }
.preview .roomstrip button[aria-pressed="true"] { border-color: var(--brass); }
.preview .roomstrip button[aria-pressed="true"] span { color: var(--ink); font-weight: 700; }

.preview .roomcats button {
  background: #fff; color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px;
}
.preview .roomcats button:hover { border-color: var(--sage); color: var(--ink); }
.preview .roomcats button[aria-pressed="true"] {
  background: var(--brass); border-color: var(--brass); color: #272823;
}

/* "Loading rooms" sits on the dark stage, so it stays light. */
.preview .loading { color: #cdc8bb; }

/* The drag handles and the wall outline, over an arbitrary photograph. The
   dark disc behind each handle is what keeps the orange readable on a pale
   wall; the outline gets the same treatment via its own dash. */
.preview .handle { border-color: var(--brass); background: rgba(39,40,35,.62); }
.preview .handle:active { background: var(--brass); }
.preview .edges polygon { stroke: var(--brass); }

/* Selection in the design picker matches the rest of the tool. */
.vis-swatches button[aria-pressed="true"] { border-color: var(--brass, #ffa038); }
.vis-swatches button[aria-pressed="true"] span { color: var(--ink); font-weight: 700; }

/* The artwork/colour segmented control: same rule, ink on the orange. */
.sb-seg button.on { background: #ffa038; color: #272823; }

/* SA-28: the quote button is the point of the whole tool and was rendering as
   a plain white pill -- the least prominent thing on a page whose job is to
   produce an enquiry. It takes the site's primary treatment.

   Deliberately SAGE, not the orange: in this tool the orange means "this is the
   one you picked", and an action is not a selection. Her logo carries both, so
   they sit together; white on `--sage-dark` measures 6.16 (SA-22b). */
.preview-quote,
#p-quote {
  background: var(--sage-dark); border-color: var(--sage-dark); color: #fff;
  font-weight: 800;
}
.preview-quote:hover,
#p-quote:hover { background: var(--sage); border-color: var(--sage); color: #fff; }

/* The slider sits in the same card as the orange segments; a second accent in
   one control group reads as unconsidered. */
.sb-row input[type="range"] { accent-color: #ffa038; }

/* SA-37: a STOCK title is an SEO keyword dump, not a name. Her own designs are
   "Golden Grains" and a Freepik result is "Marble texture seamless pattern
   Realistic marbling background for design greeting or wedding cards Black and
   white repeating template gray stone or granite" -- eight lines under a
   112px thumbnail, so the tiles ran to wildly different heights and the results
   grid was unscannable, which is the one thing it exists to be.

   Clamped, not truncated on the way in: the full title is on the button as a
   `title` attribute, so hover and assistive tech still get all of it. Same rule
   as the SA-32 shop cards -- someone else's copy is not ours to shorten. */
.vis-swatches button span {
  /* NOT `-webkit-line-clamp`. A <button>'s children sit in a button content box
     that BLOCKIFIES them, so `display: -webkit-box` computes to `flow-root` and
     the clamp silently does nothing -- measured: the property was set, applied
     to nothing, and the caption still ran to eight lines.

     An exact two-line cap instead. THE BOTTOM PADDING HAS TO COME OFF, because
     `overflow` clips at the PADDING edge, not the content edge: with 5px under
     the text the third line began 1px past the content box and still had 4px
     of itself showing -- a sliver of clipped type under every caption, which
     reads as a rendering fault rather than a limit. The breathing room moves to
     the button. 11px x 1.3 line-height = 2.6em for two lines. */
  box-sizing: border-box; padding-bottom: 0;
  max-height: calc(2.6em + 5px); overflow: hidden;
}
.vis-swatches button { padding-bottom: 5px; }
/* Feedback while a search is in flight. V1 had this and V2 could not inherit
   it: the rule lives in smartart.css, which V2 must never load (the SA-4
   rule). */
#fp-results[aria-busy="true"] { opacity: .45; }
