/* ===========================================================================
   showroom.css — the client-facing private showroom (D1c · D28 · D29)
   ---------------------------------------------------------------------------
   Namespace: .ps-*   |   This sheet NEVER loads app.css (D28).
   Authored fresh. No V3 value, no app.css value, crosses into this file (D1/D1b).

   THE GOVERNING IDEA
   The image is the page. Space goes into the photograph, not the furniture.
   Generous per-item space comes from a large slab image, not from padding —
   nine slabs at large image size is image-dense and whitespace-light.

   PREVENTION BY ABSENCE (D28's own trick: enforce by the file, not by restraint)
   This sheet deliberately has NO primitive for:
     - badge / chip / pill        -> the "25% OFF" chip cannot be rendered
     - strike-through / --was     -> comparison pricing cannot be rendered
     - card                       -> tile chrome, which is what reads as catalog
     - search / facet / filter / sort / count / paginate  -> all browse chrome
     - uppercase tracked label    -> the operator console's voice
   If you find yourself needing one of these, the requirement is wrong, not this file.
   =========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&display=swap');

:root {
  /* --- TENANT-VARIABLE. The five values a supplier may set (D29, amended). --
     Per licensed supplier, resolved at render. All that supplier's showrooms
     share them, and they are authored on showroom-branding.html.
     Brand used to reach the header and stop there; it now also reaches the
     action colour, the ground and the ink — so the G.6.4 warn/success collision
     CAN arise, and the branding page checks for it rather than the file
     preventing it. What is still unreachable is listed under RESERVED. */
  --ps-header-bg: #1C1A17;
  --ps-header-fg: #F5F3EF;
  --ps-accent:    #1C1A17;   /* .ps-action — the one action colour */
  /* Foreground on the accent, and its hover. Both DERIVED on the branding page,
     never picked by hand — a supplier cannot be trusted to keep her own CTA
     legible, and a single authored hover value is wrong for most brands. */
  --ps-accent-fg:    #F7F5F2;
  --ps-accent-hover: #35302A;

  /* --- RESERVED. Tenant theming must NEVER reach these. --------------------
     D14's semantic wins over the brand, always (G.6.4, binding). Colour here
     is a SUPPORTING channel only — never load-bearing. See .ps-state. */
  --ps-warn:    #8A5A1F;
  --ps-warn-bg: #FBF3E8;
  --ps-success:    #2F6146;
  --ps-success-bg: #EDF4EF;

  /* --- Ground. Warm stone paper, never pure white (no white-on-white). -----
     --ps-bg and --ps-ink are TENANT-VARIABLE too (D29, amended): the ground and
     the ink are the "background" and "text" the supplier sets. The derived
     values below them are not — surface, line and the two softened inks stay
     authored here, so a dark ground cannot be half-applied. */
  --ps-bg:        #F7F5F2;   /* tenant-variable */
  --ps-surface:   #FDFCFA;   /* warm white — pure #FFF reads clinical against stone */
  --ps-surface-2: #EFECE7;   /* the client's own chat bubble */
  --ps-line:      #E2DDD5;
  --ps-ink:       #1C1A17;   /* tenant-variable */
  --ps-ink-soft:  #6B655C;
  --ps-ink-mute:  #948C80;

  /* --- Type. 6 steps. 16px body vs the portal's 13px IS the density break. -
     Weights 400/500/600 only. No 700: bold is the console's table-header voice. */
  --ps-font: 'Plus Jakarta Sans', system-ui, sans-serif;
  --ps-text-hero:  40px;
  --ps-text-title: 28px;
  --ps-text-lede:  20px;
  --ps-text-body:  16px;
  --ps-text-meta:  14px;
  --ps-text-micro: 12px;
  --ps-text-price: 24px;

  /* --- Space. Base 8, range starts high, 7 steps. -------------------------- */
  --ps-space-1: 4px;
  --ps-space-2: 8px;
  --ps-space-3: 16px;
  --ps-space-4: 24px;
  --ps-space-5: 40px;
  --ps-space-6: 64px;
  --ps-space-7: 96px;

  --ps-radius: 3px;
  --ps-measure: 1180px;
}

/* --- Base ----------------------------------------------------------------- */
.ps-body {
  margin: 0;
  background: var(--ps-bg);
  color: var(--ps-ink);
  font-family: var(--ps-font);
  font-size: var(--ps-text-body);
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}
.ps-body *, .ps-body *::before, .ps-body *::after { box-sizing: border-box; }
.ps-body img { max-width: 100%; display: block; }

/* Never underline. Hover shifts colour, never decoration — to the accent, so a
   link and a button answer to the same tenant value. The header's own contact
   links keep --ps-header-fg (see .ps-contact a:hover). */
.ps-body a { color: inherit; text-decoration: none; }
.ps-body a:hover { color: var(--ps-accent); }

.ps-body :focus-visible {
  outline: 2px solid var(--ps-ink);
  outline-offset: 3px;
  border-radius: var(--ps-radius);
}

.ps-wrap { max-width: var(--ps-measure); margin: 0 auto; padding: 0 var(--ps-space-4); }

/* ===========================================================================
   HEADER — the tenant's slot, and the only place brand exists (D29)
   72px, taller and calmer than the console's 56. No nav. No search. Nothing else.
   =========================================================================== */
.ps-header {
  background: var(--ps-header-bg);
  color: var(--ps-header-fg);
  height: 72px;
  display: flex;
  align-items: center;
}
.ps-header__in {
  max-width: var(--ps-measure);
  margin: 0 auto;
  padding: 0 var(--ps-space-4);
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ps-space-4);
}

/* Constrain the slot; do not trust the asset (D29). A fixed box + contain means
   a 6:1 wordmark, a square badge and a 4000px upload all letterbox instead of
   setting the layout. The asset never gets to move anything. */
.ps-logo {
  height: 28px;
  max-width: 180px;
  width: auto;
  object-fit: contain;
  object-position: left center;
  flex: none;
}
/* Fallback when a supplier has uploaded no logo: their name, set in brand fg. */
.ps-logo-text {
  font-size: var(--ps-text-lede);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ps-header-fg);
}

.ps-contact {
  display: flex;
  align-items: center;
  gap: var(--ps-space-3);
  font-size: var(--ps-text-meta);
  color: var(--ps-header-fg);
  opacity: 0.82;
  text-align: right;
}
.ps-contact a:hover { color: var(--ps-header-fg); opacity: 1; }

/* ===========================================================================
   THE ADDRESS — she is named, and the set is a sentence, not a metric
   "Nine slabs, picked for Acme Fabricators". Never a count as a KPI (G.6.2).
   =========================================================================== */
.ps-address {
  font-size: var(--ps-text-hero);
  line-height: 1.15;
  font-weight: 500;
  letter-spacing: -0.02em;
  margin: var(--ps-space-6) 0 var(--ps-space-4);
  max-width: 20ch;
}

/* --- The rep. The page has an author and she can tell who (G.6.2). --------- */
.ps-rep { display: flex; align-items: center; gap: var(--ps-space-3); margin-bottom: var(--ps-space-6); }
.ps-rep__face {
  width: 56px; height: 56px;
  border-radius: 50%;              /* circular — exempt from the radius cap */
  object-fit: cover;
  background: var(--ps-surface-2);
  flex: none;
}
.ps-rep__name { font-weight: 500; }
.ps-rep__role { font-size: var(--ps-text-meta); color: var(--ps-ink-soft); line-height: 1.45; }

/* --- The rep's note. One paragraph, in her voice. -------------------------- */
.ps-lede {
  font-size: var(--ps-text-lede);
  line-height: 1.5;
  color: #3A3630;
  max-width: 62ch;
  margin: 0 0 var(--ps-space-6);
}

/* ===========================================================================
   THE SET — 2-up, NO CARDS. The image sits directly on the stone paper.
   Card chrome is precisely what makes a tile read as a catalog tile.
   No count, no sort, no pagination, no density toggle: each says "this is a query".
   =========================================================================== */
.ps-set {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--ps-space-6) var(--ps-space-5);
  margin-bottom: var(--ps-space-7);
}
@media (max-width: 860px) { .ps-set { grid-template-columns: 1fr; } }

.ps-item__img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--ps-radius);
  background: var(--ps-surface-2);
  margin-bottom: var(--ps-space-3);
  transition: filter 160ms ease;
}
.ps-item__name {
  font-size: var(--ps-text-title);
  line-height: 1.25;
  font-weight: 500;
  letter-spacing: -0.015em;
  margin: 0 0 var(--ps-space-1);
}
.ps-item__meta {
  font-size: var(--ps-text-meta);
  line-height: 1.45;
  color: var(--ps-ink-soft);
  margin: 0 0 var(--ps-space-3);
}

/* --- The price. ONE resolved number (D27/D32's output). -------------------
   There is no --was, no --discount, no strike-through class in this file, so
   comparison pricing is not a rule a builder can break. The absence of any
   comparison is exactly what makes it read as HERS. */
.ps-price {
  font-size: var(--ps-text-price);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* ===========================================================================
   HER SETS (D36) — the room keys on the CLIENT and is permanent; promotions
   deposit into it. So her room lists the sets she holds.

   Each set is titled by THE REP, FOR HER — never by the internal campaign name.
   That is what keeps this level legal under G.6.2: no campaign name, no
   "promotion" vocabulary, no shared-artifact tells. Bands, not cards: a card
   grid of offers is V3's discarded "All My Promotions", and reads as a menu.
   Deliberately absent: item count, status dot, date range, "% off", countdown.
   =========================================================================== */
.ps-collection { display: block; margin-bottom: var(--ps-space-6); }
.ps-collection__img {
  width: 100%;
  aspect-ratio: 16 / 6;
  object-fit: cover;
  border-radius: var(--ps-radius);
  background: var(--ps-surface-2);
  margin-bottom: var(--ps-space-3);
  transition: filter 160ms ease;
}
.ps-collection:hover .ps-collection__img { filter: brightness(1.04); }
.ps-collection__title {
  font-size: var(--ps-text-title);
  line-height: 1.25;
  font-weight: 500;
  letter-spacing: -0.015em;
  margin: 0 0 var(--ps-space-1);
}
/* Time, not lifecycle. "Added in May" is a fact about her room; "Active" and
   "Expiring Soon" are campaign lifecycle and are staff-only (G.6.1). */
.ps-collection__when { font-size: var(--ps-text-meta); color: var(--ps-ink-soft); }

/* ===========================================================================
   PRODUCT GRID — storefront-style cards for her picks (D36).
   The collection BANDS key on sets; this grid keys on the pieces themselves,
   shown the way the public storefront shows stock: image, spec line, an
   availability badge, and her one resolved Acme price. Warm ps tokens, so the
   storefront LAYOUT arrives without the storefront's cool chrome. Still one
   resolved number per piece — no strike-through, no "% off" (G.6.2).
   =========================================================================== */
.ps-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--ps-space-4);
  margin-bottom: var(--ps-space-7);
}
@media (max-width: 900px) { .ps-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .ps-grid { grid-template-columns: 1fr; } }

.ps-card {
  display: flex;
  flex-direction: column;
  background: var(--ps-surface);
  border: 1px solid var(--ps-line);
  border-radius: var(--ps-radius);
  overflow: hidden;
  cursor: pointer;
  transition: box-shadow 160ms ease, transform 160ms ease;
}
.ps-card:hover { box-shadow: 0 10px 28px rgba(28, 26, 23, 0.10); transform: translateY(-2px); }

.ps-card__img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  background: var(--ps-surface-2);
}
.ps-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--ps-space-1);
  padding: var(--ps-space-3);
  flex: 1;
}
.ps-card__name { font-size: var(--ps-text-body); font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; }
.ps-card__meta { font-size: var(--ps-text-meta); color: var(--ps-ink-soft); }
.ps-card__desc {
  font-size: var(--ps-text-meta);
  line-height: 1.5;
  color: var(--ps-ink-mute);
  margin: var(--ps-space-1) 0 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ps-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ps-space-2);
  margin-top: auto;
  padding-top: var(--ps-space-3);
}
.ps-card__price { font-size: var(--ps-text-body); font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }

/* Availability badge — the grid's one badge primitive. Colour is a support
   channel only (as with .ps-state); the dot + word carry the meaning. */
.ps-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--ps-space-1);
  padding: 3px 10px;
  border: 1px solid;
  border-radius: var(--ps-radius);
  font-size: var(--ps-text-micro);
  font-weight: 500;
  white-space: nowrap;
}
.ps-badge__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.ps-badge--success { background: var(--ps-success-bg); color: var(--ps-success); border-color: #CBE3D5; }
.ps-badge--warn    { background: var(--ps-warn-bg);    color: var(--ps-warn);    border-color: #E9D8BE; }
.ps-badge--info    { background: #EEF1F5;              color: #45566B;           border-color: #D6DEE8; }

/* ===========================================================================
   AVAILABILITY (D13) + STATE MESSAGES (D14)
   Structural first: saturation + position + copy carry the meaning.
   Colour is support only, so no brand and no colour-blindness can break it.
   =========================================================================== */

/* Taken: the pick STAYS VISIBLE, desaturated, and not requestable (D13).
   Desaturation is a non-hue channel — it survives monochrome, and it makes a
   sold slab read as gone at a glance from across the set. */
.ps-item.is-unavailable .ps-item__img { filter: grayscale(0.72) opacity(0.72); }
.ps-item.is-unavailable .ps-item__name,
.ps-item.is-unavailable .ps-price { color: var(--ps-ink-soft); }
/* The action is REMOVED, not disabled-looking (D13: "not requestable"). */
.ps-item.is-unavailable .ps-action { display: none; }

/* Bands, never badges. D14: "explicit, not a subtle badge" — and there is no
   badge class here anyway. Fixed position directly under the image. */
.ps-state {
  display: flex;
  align-items: flex-start;
  gap: var(--ps-space-2);
  padding: var(--ps-space-2) var(--ps-space-3);
  border-radius: var(--ps-radius);
  border-left: 3px solid;
  font-size: var(--ps-text-meta);
  line-height: 1.45;
  margin-bottom: var(--ps-space-3);
}
.ps-state__icon { flex: none; width: 16px; height: 16px; margin-top: 2px; }

/* Taken — another client took it. The buyer's EXISTENCE is disclosed, their
   identity is not (D14: confidentiality is settled by the copy). */
.ps-state--warn { background: var(--ps-warn-bg); border-left-color: var(--ps-warn); color: var(--ps-warn); }
/* Returned — available again. The module's strongest trigger (D14). This is the
   page's own truth and does not depend on D15's email having gone out. */
.ps-state--success { background: var(--ps-success-bg); border-left-color: var(--ps-success); color: var(--ps-success); }

/* ===========================================================================
   ACTIONS — ask / quote / sample / visit. Never ordering.
   Labels are [OPEN] (a copy decision); the primitive is not.
   Ink, not brand: brand is header-only, so the CTA is brand-independent and
   therefore safe under every supplier's palette.
   =========================================================================== */
.ps-action {
  display: inline-flex;
  align-items: center;
  gap: var(--ps-space-2);
  background: var(--ps-accent);
  color: var(--ps-accent-fg);
  border: 0;
  border-radius: var(--ps-radius);
  padding: 11px var(--ps-space-4);
  font-family: inherit;
  font-size: var(--ps-text-body);
  font-weight: 500;
  cursor: pointer;
  transition: background 140ms ease;
}
.ps-action:hover { background: var(--ps-accent-hover); }
.ps-action--quiet { background: transparent; color: var(--ps-ink); border: 1px solid var(--ps-line); }
.ps-action--quiet:hover { background: var(--ps-surface-2); }

/* ===========================================================================
   PRODUCT VIEW — the same language, one image bigger (Q-A15 form is [OPEN])
   =========================================================================== */
.ps-product { display: grid; grid-template-columns: 1.5fr 1fr; gap: var(--ps-space-6); padding: var(--ps-space-6) 0; }
@media (max-width: 860px) { .ps-product { grid-template-columns: 1fr; } }
.ps-product__img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--ps-radius); }
.ps-spec { display: grid; grid-template-columns: auto 1fr; gap: var(--ps-space-2) var(--ps-space-4); font-size: var(--ps-text-meta); }
.ps-spec dt { color: var(--ps-ink-mute); }        /* sentence case — never tracked caps */
.ps-spec dd { margin: 0; color: var(--ps-ink); font-variant-numeric: tabular-nums; }

/* ===========================================================================
   CHAT — the THIRD chat surface. Not the rep's cockpit, not support.html.
   Authorship is POSITION, not hue (G.5 axis 9 — the argument crosses, the
   class does not). Brand cannot tint the bubble, which closes axis 9's [OPEN].
   =========================================================================== */
.ps-chat { background: var(--ps-surface); border: 1px solid var(--ps-line); border-radius: var(--ps-radius); display: flex; flex-direction: column; }
.ps-chat__log { padding: var(--ps-space-4); display: flex; flex-direction: column; gap: var(--ps-space-3); }

.ps-msg { max-width: 78%; padding: var(--ps-space-2) var(--ps-space-3); border-radius: var(--ps-radius); font-size: var(--ps-text-body); }
.ps-msg--them { align-self: flex-start; background: transparent; padding-left: 0; }  /* the rep: flat on the surface */
.ps-msg--me   { align-self: flex-end;   background: var(--ps-surface-2); }           /* hers: neutral tint + position */
.ps-msg__time { font-size: var(--ps-text-micro); color: var(--ps-ink-mute); margin-top: var(--ps-space-1); }

/* Offline honesty, stated BEFORE she types — and she can still send (D20).
   A quiet line, not a coloured banner: this is a state, not an instruction.
   Presence fails to offline, always (D34) — never render online on stale data. */
.ps-chat__presence {
  padding: var(--ps-space-2) var(--ps-space-4);
  border-top: 1px solid var(--ps-line);
  font-size: var(--ps-text-meta);
  color: var(--ps-ink-soft);
}
.ps-chat__composer { display: flex; gap: var(--ps-space-2); padding: var(--ps-space-3) var(--ps-space-4) var(--ps-space-4); }
.ps-chat__input {
  flex: 1;
  font-family: inherit;
  font-size: var(--ps-text-body);
  color: var(--ps-ink);
  background: var(--ps-bg);
  border: 1px solid var(--ps-line);
  border-radius: var(--ps-radius);
  padding: 10px var(--ps-space-3);
  resize: none;
}
.ps-chat__input::placeholder { color: var(--ps-ink-mute); }

/* ===========================================================================
   THE FRONT DOOR (D11) — the showroom mints its own auth; it inherits none.
   Named and rep-signed: this is her FIRST touch of the premise, so it does the
   work an anonymous code box cannot.
   =========================================================================== */
.ps-gate { min-height: 100vh; display: grid; place-items: center; padding: var(--ps-space-5); }
.ps-gate__in { width: 100%; max-width: 440px; text-align: center; }
.ps-gate__title { font-size: var(--ps-text-title); line-height: 1.25; font-weight: 500; letter-spacing: -0.015em; margin: 0 0 var(--ps-space-2); }
.ps-gate__note { font-size: var(--ps-text-meta); color: var(--ps-ink-soft); margin: 0 0 var(--ps-space-5); }
.ps-code {
  width: 100%;
  font-family: inherit;
  font-size: var(--ps-text-title);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.18em;
  text-align: center;
  color: var(--ps-ink);
  background: var(--ps-surface);
  border: 1px solid var(--ps-line);
  border-radius: var(--ps-radius);
  padding: var(--ps-space-3);
  margin-bottom: var(--ps-space-3);
}
.ps-code.is-invalid { border-color: var(--ps-warn); }

/* ===========================================================================
   FOOTER — attribution, not presence (D29)
   .ps-footer__vava is one class, so the plan-gated-removal [OPEN] is a
   one-line change whichever way it lands.
   =========================================================================== */
.ps-footer {
  border-top: 1px solid var(--ps-line);
  padding: var(--ps-space-5) 0;
  margin-top: var(--ps-space-7);
  font-size: var(--ps-text-micro);
  color: var(--ps-ink-mute);
  display: flex;
  justify-content: space-between;
  gap: var(--ps-space-4);
  flex-wrap: wrap;
}
.ps-footer__vava { color: var(--ps-ink-mute); }
