/* Usher — tenant-skinnable chat surface.
   Every colour/type decision is a token, so a tenant skin is a variable swap
   (spec §4 persona pack → brand tokens). Defaults below are the house neutral.

   The tokens are set at runtime from the tenant's own persona record
   (GET /usher/persona → cssVars), NOT from a per-tenant stylesheet. That is
   what makes a new tenant a config write rather than a deploy: the second
   tenant used to render in the first one's clothes because the skin was a
   static <link> in index.html.

   Anything the Apothekary skin once overrode has a token here, so her look is
   reproducible from config alone — which is the proof the token surface is
   expressive enough for a real brand rather than just a palette swap. */

:root{
  --bg:#0E1116;
  --surface:#151A21;
  --card:#1C232C;
  --raised:#232C37;
  --hairline:#2A3541;
  --fg:#ECEEF1;
  --muted:#98A2B0;
  --faint:#6B7684;
  --accent:#E8833A;
  --accent-ink:#12161B;
  --accent-wash:rgba(232,131,58,.12);
  --good:#4FB39A;
  --radius:14px;
  --radius-sm:10px;
  --font-body:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --shadow:0 1px 2px rgba(0,0,0,.3),0 8px 28px rgba(0,0,0,.32);
  --maxw:760px;

  /* Type + shape. A tenant that sets none of these still looks deliberate. */
  --font-label:var(--font-body);
  --offer-title-font:var(--font-body);
  --body-size:16px;
  --title-size:1.7rem;      --title-weight:700;  --title-tracking:-.02em;  --title-lh:1.1;
  --pre-size:12px;          --pre-tracking:.14em;  --pre-color:var(--faint);
  --tag-size:.95rem;        --tag-style:normal;    --tag-color:var(--accent);
  --blurb-size:.93rem;
  --label-size:.95rem;      --label-tracking:0em;  --label-transform:none;
  --path-desc-size:.85rem;  --path-rule-width:0px;
  --offer-title-size:1rem;  --offer-title-weight:650;  --offer-title-tracking:-.01em;
  --offer-body-size:.9rem;  --offer-rule-width:3px;    --offer-bullet:none;
  --badge-size:10.5px;      --badge-tracking:.06em;    --chip-radius:20px;
  --cta-size:.88rem;        --cta-tracking:0em;        --cta-radius:8px;
  --fine-size:.78rem;
  --av-size:11px;           --av-tracking:.02em;
  --rule-width:0px;         --field-radius:var(--radius);
  --accent-blunt:#E5484D;   --accent-blunt-wash:rgba(229,72,77,.12);
}
:root[data-theme="light"]{
  --bg:#F5F3F0; --surface:#FFFFFF; --card:#FFFFFF; --raised:#F0EDE9;
  --hairline:#E2DDD6; --fg:#1A1D21; --muted:#5C646E; --faint:#8B9199;
  --accent:#C8631C; --accent-ink:#FFFFFF; --accent-wash:rgba(200,99,28,.09);
  --shadow:0 1px 2px rgba(20,24,28,.06),0 8px 24px rgba(20,24,28,.08);
}

*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;background:var(--bg);color:var(--fg);
  font-family:var(--font-body);font-size:var(--body-size);line-height:1.55;
  -webkit-font-smoothing:antialiased;
}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* Fixed viewport height so .chat scrolls internally — otherwise the whole
   page scrolls and the composer walks off the bottom. dvh handles mobile
   browser chrome; vh is the fallback. */
.app{
  max-width:var(--maxw);margin:0 auto;
  height:100vh;height:100dvh;overflow:hidden;
  display:flex;flex-direction:column;padding:0 16px;
}
.hd{flex:0 0 auto}

/* ── header ─────────────────────────────────────────── */
.hd{display:flex;gap:16px;align-items:flex-start;padding:26px 4px 18px;position:relative}
/* Accent rule under the masthead. Zero-width by default, so a tenant opts in
   by setting --rule-width rather than the house carrying someone's flourish. */
.hd::after{
  content:"";position:absolute;left:0;bottom:-1px;height:1px;width:var(--rule-width);
  background:linear-gradient(90deg,var(--accent),transparent);
}
.hd-avatar{
  width:60px;height:60px;border-radius:50%;flex:0 0 60px;
  background:linear-gradient(145deg,var(--raised),var(--card));
  border:1px solid var(--hairline);background-size:cover;background-position:center;
  /* Initials when there is no upload — the same fallback the chat avatar has. */
  display:grid;place-items:center;font-family:var(--font-label);
  font-size:calc(var(--av-size) * 1.5);font-weight:700;
  letter-spacing:var(--av-tracking);color:var(--muted);
}
/* Start over, in the bar the masthead collapses to. Hidden until then: on
   first paint there is nothing to start over from. */
.hd-restart{
  display:none;margin-left:auto;align-self:center;white-space:nowrap;
  font-family:var(--font-label);font-size:var(--fine-size);
  letter-spacing:var(--label-tracking);text-transform:var(--label-transform);
  color:var(--muted);text-decoration:none}
.hd-restart:hover{color:var(--accent)}
.app.talking .hd-restart{display:inline-block}
.app.talking .hd-avatar{font-size:var(--av-size)}
.hd-text{min-width:0}
.hd-pre{
  margin:0;font-family:var(--font-label);font-size:var(--pre-size);
  letter-spacing:var(--pre-tracking);text-transform:uppercase;color:var(--pre-color);
}
.hd-title{
  margin:.12em 0 .1em;font-family:var(--font-display);font-size:var(--title-size);
  line-height:var(--title-lh);letter-spacing:var(--title-tracking);
  font-weight:var(--title-weight);text-wrap:balance;
}
.hd-tag{
  margin:0;color:var(--tag-color);font-size:var(--tag-size);
  font-family:var(--font-display);font-style:var(--tag-style);
}
.hd-blurb{margin:.5em 0 0;color:var(--muted);font-size:var(--blurb-size);max-width:56ch}

/* ── chat ───────────────────────────────────────────── */
/* R2. .chat is a fixed band between two pieces of chrome, and on a 375x812
   phone it gets 288px for 457px of greeting and paths — two of the three ways
   into the conversation sat below an edge whose only cue was a hairline
   scrollbar. The masthead trim recovers most of it (that is a tenant's copy,
   not ours to fix here); this is the half that belongs in the house.

   The classic two-layer scroll shadow: the `local` layer scrolls WITH the
   content and covers the `scroll` layer, which is pinned to the band. At the
   end of the list the two line up and the fade disappears on its own, so a
   list that is fully read never wears a false "more below". No JS, and no
   state for either renderer to get wrong. */
.chat{
  flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:16px;
  padding:8px 0 20px;overflow-y:auto;
  background:
    linear-gradient(to top,var(--bg),transparent) bottom center / 100% 26px no-repeat local,
    linear-gradient(to top,var(--bg),transparent) bottom center / 100% 26px no-repeat scroll;
}
.msg{display:flex;gap:11px;align-items:flex-start;animation:rise .26s ease both}
@keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.msg{animation:none}}
.msg.user{flex-direction:row-reverse}
.av{
  flex:0 0 30px;width:30px;height:30px;border-radius:50%;
  display:grid;place-items:center;font-family:var(--font-label);
  font-size:var(--av-size);font-weight:700;letter-spacing:var(--av-tracking);
  background:var(--raised);color:var(--muted);border:1px solid var(--hairline);
  background-size:cover;background-position:center;
}
.msg.bot .av{background:var(--accent);color:var(--accent-ink);border-color:transparent}
.body{
  background:var(--surface);border:1px solid var(--hairline);
  border-radius:var(--radius);padding:12px 15px;max-width:min(84%,58ch);
}
.msg.user .body{background:var(--accent-wash);border-color:transparent}
.body p{margin:0 0 .6em}
.body p:last-child{margin-bottom:0}
.body strong{font-weight:600}

/* typing */
.dots{display:inline-flex;gap:4px;padding:3px 0}
.dots i{width:6px;height:6px;border-radius:50%;background:var(--faint);animation:blink 1.3s infinite}
.dots i:nth-child(2){animation-delay:.18s}.dots i:nth-child(3){animation-delay:.36s}
@keyframes blink{0%,60%,100%{opacity:.25}30%{opacity:1}}
@media (prefers-reduced-motion:reduce){.dots i{animation:none;opacity:.5}}
.slow{color:var(--faint);font-size:.82rem;margin-top:6px}

/* ── quick paths ────────────────────────────────────── */
.paths{display:flex;flex-direction:column;gap:8px;margin:12px 0 4px}
/* R3. A path is a CHOICE, and it has to look like one before it is touched.
   The label is set in the tenant's label face — for two of the three tenants
   that is uppercase mono with wide tracking, i.e. the same treatment as
   .hd-pre and .wall-lead, which are inert. Rest state carried no other cue:
   the left rule was `transparent` until :hover, and :hover does not exist on
   the device most of these visitors are holding.

   Two changes. The rule is lit at rest (still zero-width unless the tenant
   opted in with --path-rule-width, so no tenant gains a stripe it did not
   ask for), and every path gets a chevron. Grid, not flex, so the chevron
   can sit in its own column beside both lines without either renderer having
   to emit an extra element. */
.path{
  text-align:left;cursor:pointer;font:inherit;color:var(--fg);
  background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius-sm);
  padding:11px 13px;
  display:grid;grid-template-columns:minmax(0,1fr) auto;
  column-gap:12px;row-gap:2px;align-items:center;
  border-left:var(--path-rule-width) solid color-mix(in srgb,var(--accent) 55%,transparent);
  transition:border-color .15s,background .15s,transform .1s;
}
.path > b{grid-column:1;grid-row:1}
.path > span{grid-column:1;grid-row:2}
.path::after{
  content:"";grid-column:2;grid-row:1 / span 2;align-self:center;
  width:7px;height:7px;margin-right:2px;
  border-right:2px solid var(--accent);border-top:2px solid var(--accent);
  transform:rotate(45deg);opacity:.8;
  transition:opacity .15s,transform .15s}
.path:hover::after{opacity:1;transform:rotate(45deg) translate(1px,-1px)}
@media (prefers-reduced-motion:reduce){
  .path::after,.path:hover::after{transition:none;transform:rotate(45deg)}
}
.path:hover{border-color:var(--accent);background:var(--raised)}
.path:active{transform:translateY(1px)}
.path b{
  font-family:var(--font-label);font-weight:600;font-size:var(--label-size);
  letter-spacing:var(--label-tracking);text-transform:var(--label-transform);
}
.path span{color:var(--muted);font-size:var(--path-desc-size);line-height:1.4}

/* ── offer card ─────────────────────────────────────── */
.offer{
  background:var(--card);border:1px solid var(--hairline);
  border-left:var(--offer-rule-width) solid var(--accent);
  border-radius:var(--radius-sm);padding:13px 15px;margin:11px 0;box-shadow:var(--shadow);
}
/* A link card is a tap target end to end, not just its button. */
.offer-link{cursor:pointer;transition:border-color .15s}
.offer-link:hover{border-color:var(--accent)}
.offer:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* R5. nowrap + margin-left:auto parks the badge top-right on EVERY card and
   lets the title wrap under it. It used to sit beside a short title and drop
   below a long one, which in a 320px rail flips from card to card — a column
   of four reads ragged when the one repeated element will not hold a line.
   Baseline, not center, so the badge sits on the title's first line. */
.offer-top{display:flex;align-items:baseline;gap:9px;flex-wrap:nowrap;margin-bottom:3px}
.offer-top h4{min-width:0}
.offer-top .badge{margin-left:auto;flex:0 0 auto}
.offer h4{
  margin:0;font-family:var(--offer-title-font);font-size:var(--offer-title-size);
  font-weight:var(--offer-title-weight);letter-spacing:var(--offer-title-tracking);
}
/* Tenant glyph before an offer title — empty by default, so no pseudo-element
   box is painted unless a tenant asks for one. */
.offer h4::before{
  content:var(--offer-bullet);color:var(--accent);
  font-size:.62em;vertical-align:.18em;margin-right:.5em;
}
.badge{
  font-family:var(--font-label);font-size:var(--badge-size);
  letter-spacing:var(--badge-tracking);text-transform:uppercase;font-weight:600;
  padding:2px 7px;border-radius:var(--chip-radius);border:1px solid var(--hairline);color:var(--muted);
}
.badge[data-s="free"]{color:var(--good);border-color:color-mix(in srgb,var(--good) 45%,transparent)}
.badge[data-s="open-window"]{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)}
.offer p{margin:0;color:var(--muted);font-size:var(--offer-body-size)}
.cta{
  display:inline-block;margin-top:10px;padding:7px 13px;border-radius:var(--cta-radius);
  background:var(--accent);color:var(--accent-ink);text-decoration:none;
  font-family:var(--font-label);font-size:var(--cta-size);letter-spacing:var(--cta-tracking);
  text-transform:var(--label-transform);font-weight:600;transition:filter .15s;
}
.cta:hover{filter:brightness(1.08)}

/* R4. The solid fill is the loudest thing the palette has, and it was on the
   CTA of every card at every weight — eighteen of them in sellmyservice's
   phone sheet, plus #send, which is the control the whole surface is built
   around. A colour that is on everything points at nothing.

   So the fill follows the weight the wall already computes: filled on the
   feature, outlined on the substance, a plain link on the free entry points.
   The line card keeps its vertical padding — the text shrinks, the target
   does not — and the whole card is a tap target anyway (.offer-link). */
.offer-standard > .cta{
  background:transparent;color:var(--accent);
  border:1px solid var(--accent)}
.offer-standard > .cta:hover{
  background:color-mix(in srgb,var(--accent) 12%,transparent);filter:none}
.offer-line > .cta{
  background:transparent;color:var(--accent);
  padding-left:0;padding-right:0}
.offer-line > .cta:hover{
  filter:none;text-decoration:underline;text-underline-offset:3px}

/* ── offer wall (hybrid surface) ────────────────────── */
/* No indent. The 41px that lined the shelf up under the bot bubbles stopped
   lining up with anything when the greeting left its bubble (F7): the copy
   above the shelf is flush left, so the shelf is too. */
.wall{margin:4px 0 2px;padding-left:0}
.wall-lead{
  margin:0 0 2px;font-family:var(--font-label);font-size:var(--fine-size);
  letter-spacing:var(--label-tracking);text-transform:var(--label-transform);
  color:var(--faint);
}
.wall-ask{margin:10px 0 0;font-size:var(--fine-size);color:var(--faint)}
.wall-ask a{color:var(--accent)}
/* the no-tenant state: no persona, so no avatar to show */
body.no-tenant .hd-avatar{display:none}

/* ── the moment (Floor Plans, Fittings: "capture as a moment") ────────
   Capture used to be a dashed rectangle stapled inside the bubble — a dashed
   button, two stacked inputs, a checkbox and Send/Cancel at fine-print size.
   It is now its own object UNDER the message, at the column's width (indented
   past the avatar so it lines up with the bubble), with the operator's
   captureLabel as the heading, their optInCopy at reading size, and one field.
   The handoff is the same object with a named human in place of the heading. */
.moment{
  margin:-4px 0 0 41px;padding:16px 18px 14px;
  background:var(--surface);border:1px solid var(--hairline);
  border-left:2px solid var(--accent);border-radius:var(--radius);
  display:flex;flex-direction:column;gap:10px;max-width:min(88%,58ch);
  animation:rise .26s ease both}
@media (prefers-reduced-motion:reduce){.moment{animation:none}}
@media(max-width:560px){.moment{max-width:none;margin-left:41px}}
.moment-h{
  margin:0;font-family:var(--font-display);
  font-size:calc(var(--offer-title-size,1rem) * 1.05);
  font-weight:var(--offer-title-weight);line-height:1.2}
.moment-form{display:flex;flex-direction:column;gap:10px}
.moment-row{display:flex;gap:8px}
.moment-row input{
  flex:1;min-width:0;font:inherit;font-size:.95rem;padding:11px 12px;
  border-radius:var(--field-radius);background:var(--bg);
  border:1px solid var(--hairline);color:var(--fg)}
.moment-row input:focus{outline:2px solid var(--accent);outline-offset:2px}
.moment-opt{
  display:flex;gap:9px;align-items:flex-start;margin:0;
  font-size:var(--offer-body-size);color:var(--muted);line-height:1.45}
.moment-opt input{margin-top:3px}
/* the handoff's named human */
.moment-who{display:flex;gap:11px;align-items:center}
.moment-who .av{
  flex-basis:34px;width:34px;height:34px;
  background:var(--accent);color:var(--accent-ink);border-color:transparent}
.moment-who b{
  display:block;font-family:var(--font-display);
  font-size:calc(var(--offer-title-size,1rem) * 1.05);
  font-weight:var(--offer-title-weight);line-height:1.15}
.moment-who span{display:block;color:var(--muted);font-size:var(--fine-size);margin-top:2px}
.cap-send{
  cursor:pointer;font:inherit;font-family:var(--font-label);font-size:var(--cta-size);
  letter-spacing:var(--cta-tracking);text-transform:var(--label-transform);font-weight:600;
  background:var(--accent);color:var(--accent-ink);border:none;border-radius:var(--cta-radius);padding:8px 16px;
}
.cap-send:disabled{opacity:.6;cursor:default}
.cap-status{font-size:.85rem;color:var(--muted)}
.cap-status:empty{display:none}
.cap-status.ok{color:var(--good)}
.hidden{display:none !important}

/* ── composer ───────────────────────────────────────── */
.composer{position:sticky;bottom:0;background:var(--bg);padding:10px 0 14px;border-top:1px solid var(--hairline)}
.composer-in{display:flex;gap:9px;align-items:flex-end}
textarea#input{
  flex:1;font:inherit;font-size:.97rem;resize:none;max-height:120px;
  padding:11px 13px;border-radius:var(--field-radius);
  background:var(--surface);border:1px solid var(--hairline);color:var(--fg);
}
textarea#input:focus,input[type=email]:focus,.path:focus-visible,button:focus-visible,a:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;
}
#send{
  flex:0 0 42px;height:42px;border-radius:var(--field-radius);cursor:pointer;
  background:var(--accent);color:var(--accent-ink);border:none;display:grid;place-items:center;
}
#send:disabled{opacity:.45;cursor:default}
.hint,.privacy{margin:7px 2px 0;font-size:var(--fine-size);color:var(--faint);line-height:1.45}
.privacy a{color:var(--muted)}
.privacy b{font-weight:600;color:var(--muted)}
.privacy small{display:block;font-size:inherit}
/* The hint suggests what to type; once something has been typed it has done
   its job, and the composer has the visitor. */
.app.talking .hint{display:none}

/* feral/blunt mode — a tenant "mode" (spec §4 modes) restyles the surface */
body.mode-blunt{--accent:var(--accent-blunt);--accent-wash:var(--accent-blunt-wash)}
body.mode-blunt .hd-title{letter-spacing:-.03em}

@media(max-width:560px){
  .hd{padding-top:18px}
  .hd-avatar{width:48px;height:48px;flex-basis:48px}
  .hd-title{font-size:1.42rem}
  .body{max-width:88%}
}

/* §2.5 — a URL that appeared in prose. Never a link; shown as inert text so
   the visitor can see it was not endorsed as a destination. */
.inert-url{
  color:var(--faint);
  text-decoration:line-through;
  text-decoration-color:color-mix(in srgb,var(--faint) 55%,transparent);
  word-break:break-all;
}

/* A tenant's own logo, above the name it belongs to. Sized by height so a
   wide wordmark and a square mark both sit right, and capped so a large
   upload cannot push the conversation off the first screen. */
.hd-logo{display:block;max-height:38px;max-width:min(220px,60%);width:auto;height:auto;
  margin:0 0 10px;object-fit:contain}
@media (max-width:560px){ .hd-logo{max-height:30px} }

/* ── rich embeds (T0.4) ───────────────────────────────────────────────
   A wrapper carries the shape and the iframe simply fills it, so a video
   keeps 16/9 while a booking form takes a fixed height — the provider
   decides which, and the card never has to know.

   The frame is sized in PERCENT rather than pixels because the card it sits
   in is fluid, and it gets the card's own radius so an embed reads as part
   of the offer rather than pasted on top of it. */
.embed{margin:10px 0 0;width:100%;overflow:hidden;border-radius:8px;
  background:var(--raised);border:1px solid var(--hairline)}
.embed iframe{display:block;width:100%;height:100%;border:0}

/* ── the prescription block (Floor Plans F2) ──────────────────────────
   A routed reply has to read as a different KIND of object than chat, or
   the one thing this product does that a link list cannot is invisible.
   The frame does that work: a rule down the accent side, its own ground,
   and the cards sitting inside it rather than after it. */
.rx{
  border-left:2px solid var(--accent);
  background:var(--accent-wash);
  border-radius:0 8px 8px 0;
  padding:14px 14px 6px 16px;
  margin:2px 0 0;
  display:flex;flex-direction:column;gap:10px}
.rx-finding{margin:0}
.rx .offer{background:var(--card);margin:0;
  /* The frame's rule is the rule. A card inside it keeps its shape but not
     its own accent edge, or two rules sit 16px apart saying the same thing. */
  border-left-width:0}

/* The rank is a small ordinal, not a badge — it says WHERE this sits, and
   the badge beside the title already says what it is. */
.offer-rank{
  display:block;
  font-size:var(--fine-size);
  letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--accent);
  margin-bottom:4px}

/* The reason, in the tenant's own voice.
   ⚠️ It has to read as a DIFFERENT VOICE from the tagline directly above it —
   the offer describing itself, then the operator saying why it is the answer.
   Set in the same words they blur into one paragraph and the because is lost,
   which is the whole point of moving it here. Italic does that work in one
   property and inherits whatever display face the tenant set. */
.offer-because{
  margin:11px 0 0;
  color:var(--fg);
  font-size:var(--offer-body-size);
  font-style:italic;
  line-height:1.5}
.offer-because::before{
  content:"";
  display:block;
  width:22px;
  height:1px;
  background:var(--accent);
  opacity:.55;
  margin:0 0 8px}

/* ── the collapsing masthead (Floor Plans F3, F4) ─────────────────────
   Measured on a 375x812 phone: avatar, prefix, name, tagline and blurb are
   277px — 34% of the screen — and they never move. On turn six a third of
   the page is still introducing someone the visitor is already talking to,
   while the conversation lives in the 361px left over.

   So the masthead is full on first paint and becomes a 44px bar from turn
   one. It stays a HEADER rather than disappearing: the visitor should always
   be able to see whose page this is.

   Driven by a class on .app, not by scroll position — the trigger is "the
   conversation has started", which is a state, not a distance. */
.app.talking .hd{padding:10px 4px 8px;gap:11px;align-items:center}
.app.talking .hd-avatar{width:30px;height:30px;flex-basis:30px}
.app.talking .hd-logo{max-height:24px;margin:0}
.app.talking .hd-title{
  font-size:clamp(15px,4vw,18px);
  line-height:1.2;margin:0;letter-spacing:0}
/* Collapsed to zero rather than display:none — the height animates, and a
   hidden node keeps its text available to a screen reader mid-sentence. */
.app.talking .hd-pre,
.app.talking .hd-tag,
.app.talking .hd-blurb{
  height:0;margin:0;opacity:0;overflow:hidden;pointer-events:none}

.hd,.hd-avatar,.hd-title,.hd-pre,.hd-tag,.hd-blurb{
  transition:height .22s ease,opacity .18s ease,padding .22s ease,
             width .22s ease,font-size .22s ease}
@media (prefers-reduced-motion:reduce){
  .hd,.hd-avatar,.hd-title,.hd-pre,.hd-tag,.hd-blurb{transition:none}
}

/* ── the greeting, out of its bubble (F7) ─────────────────────────────
   Page -> chat log -> message bubble -> path button was three borders deep,
   and the greeting is the most carefully written copy on the page. It is
   the operator addressing the room, not a chat turn, so it is set as page
   copy: no avatar, no bubble, full column width. Every LATER turn keeps its
   bubble, because from then on it genuinely is a conversation. */
.greeting{max-width:62ch}
.greeting p{margin:0 0 .7em;font-size:var(--greeting-size,1.02rem);line-height:1.6}
.greeting p:last-of-type{margin-bottom:0}
.greeting .paths{margin-top:14px}

/* ── card weights, price and the expanded card (F1, F5) ───────────────
   Weight is derived, never authored — see wallOrder/weightFor in app.js.
   Three shapes, and the difference has to be legible at a glance or the
   ranking does no work:

   feature   the first card, one per view — full size, price shown large
   standard  the substance
   line      free entry points: present, deliberately lighter */
.wall-cards{display:flex;flex-direction:column;gap:10px}

.offer-feature{border-color:var(--accent);background:var(--raised)}
.offer-feature h4{font-size:calc(var(--offer-title-size,1rem) * 1.1)}

.offer-line{padding-top:11px;padding-bottom:11px;opacity:.9}
.offer-line h4{font-size:calc(var(--offer-title-size,1rem) * .95)}
.offer-line p{font-size:var(--fine-size)}

/* Price as type, in the tenant's display face — not a chip beside a badge. */
.offer-price{
  margin:8px 0 0;font-family:var(--font-display);
  font-size:calc(var(--offer-title-size,1rem) * 1.05);
  color:var(--fg);letter-spacing:-.01em}
.offer-feature .offer-price{font-size:calc(var(--offer-title-size,1rem) * 1.25)}

/* includes[] — reveals in place, so checking what is in a thing never costs
   you the page you were reading. */
.offer-includes{margin:10px 0 0}
.offer-includes summary{
  cursor:pointer;color:var(--accent);font-size:var(--fine-size);
  letter-spacing:.04em;list-style:none}
.offer-includes summary::-webkit-details-marker{display:none}
.offer-includes summary::before{content:"+ "}
.offer-includes[open] summary::before{content:"\2212 "}
.offer-includes ul{margin:8px 0 0;padding-left:18px;display:flex;flex-direction:column;gap:5px}
.offer-includes li{color:var(--muted);font-size:var(--offer-body-size)}
.offer-includes li::marker{color:var(--accent)}

/* The handle on a capped wall. Not a button-looking thing — it reveals what
   is already on the page rather than navigating anywhere — but with the cap
   at four it is the only way into most of the shelf, so it is set in the CTA
   size and the label face above a hairline, not in the fine print under the
   cards. */
.wall-more{
  display:block;width:100%;margin:14px 0 0;padding:10px 0 0;border:0;
  border-top:1px solid var(--hairline);background:none;
  font:inherit;font-family:var(--font-label);font-size:var(--cta-size);
  letter-spacing:var(--cta-tracking);text-transform:var(--label-transform);font-weight:600;
  color:var(--accent);cursor:pointer;text-align:left}
.wall-more:hover{text-decoration:underline;text-underline-offset:3px}

/* The server renders every offer so a crawler indexes the whole catalog, and
   the overflow is hidden here rather than omitted. The client's own render
   replaces this wrapper with a capped list and a working "See all" control,
   so this rule only governs the moments before hydration — including the case
   where hydration never arrives because the API is down. */
.wall-cards-ssr > .offer:nth-child(n+5){display:none}

/* ══ LAYOUTS ═════════════════════════════════════════════════════════
   Four rooms, one skeleton. The markup does not change between them —
   masthead, chat log, composer — so a layout is CSS plus, for Desk, one
   extra element the client moves the wall into. A closed set of four
   (usher-brand.ts), never free-form, because this is painted from KV
   onto a public page.

   `column` is the default and needs no rules: it is what ships today. */

/* ── DESK ────────────────────────────────────────────────────────────
   Conversation left, a standing catalog rail right — always visible,
   scrolling in its own column, never competing with the chat for the
   same pixels. The wall stays on the page for the visitor who came for
   a list, and the composer stays for the one who came with a question,
   so neither is buried behind the other. */
@media (min-width:900px){
  .app.layout-desk{
    max-width:1080px;
    display:grid;
    grid-template-columns:minmax(0,1fr) 320px;
    grid-template-rows:auto minmax(0,1fr) auto;
    grid-template-areas:"head rail" "chat rail" "comp rail";
    column-gap:28px}
  .layout-desk .hd{grid-area:head}
  .layout-desk .chat{grid-area:chat}
  .layout-desk .composer{grid-area:comp}
  .layout-desk .rail{
    grid-area:rail;
    min-height:0;overflow-y:auto;
    padding:26px 0 20px;
    border-left:1px solid var(--hairline);
    padding-left:22px}
  .layout-desk .rail .wall{margin:0}
  /* R7. "See all 18" is the ONLY route to everything past the cap, and it sat
     at the bottom of a column taller than the window — at 1280x860 the rail
     wanted 977px, so the door was off-screen in the one column that had room
     to spare. Stuck to the floor of the rail it cannot be the thing that
     scrolls away. --bg, not transparent, so cards pass underneath it. */
  .layout-desk .rail .wall-more{
    position:sticky;bottom:0;z-index:1;
    background:var(--bg);
    margin-bottom:0;padding-bottom:10px}
}
/* The entries the model just named, lit in place. Same label style as the
   prescription's ordinal, so the two views of the catalog read as one. */
.rail .offer.routed{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-wash),var(--shadow)}
.rail .offer.routed::before{
  content:"Recommended";display:block;
  font-size:var(--fine-size);letter-spacing:.12em;text-transform:uppercase;
  color:var(--accent);margin-bottom:4px}

/* On a phone the rail becomes a sheet: a persistent handle that pulls up
   over the conversation, so twelve offers are one tap away instead of
   six screens of scrolling. */
@media (max-width:899px){
  .layout-desk .rail{
    position:fixed;left:0;right:0;bottom:0;z-index:40;
    max-height:76vh;overflow-y:auto;
    background:var(--surface);
    border-top:1px solid var(--hairline);
    border-radius:14px 14px 0 0;
    box-shadow:0 -8px 30px rgba(0,0,0,.28);
    padding:0 16px 18px;
    transform:translateY(calc(100% - 52px));
    transition:transform .26s ease}
  .layout-desk .rail.open{transform:translateY(0)}
  .layout-desk .rail-handle{
    position:sticky;top:0;
    display:flex;align-items:center;justify-content:space-between;gap:12px;
    width:100%;height:52px;padding:0;
    background:var(--surface);border:0;
    font:inherit;font-size:var(--fine-size);letter-spacing:.06em;
    text-transform:uppercase;color:var(--accent);cursor:pointer}
  .layout-desk .rail .wall-lead{display:none}
  /* The closed sheet leaves 52px of handle over the bottom of the viewport,
     and the composer is sticky to that same bottom — measured at 390, the
     handle sat on the privacy line. The composer keeps the handle's height
     clear under it. */
  .layout-desk .composer{padding-bottom:calc(14px + 52px)}
  @media (prefers-reduced-motion:reduce){ .layout-desk .rail{transition:none} }
}
.rail-handle{display:none}
@media (max-width:899px){ .layout-desk .rail-handle{display:flex} }

/* ── FOYER ───────────────────────────────────────────────────────────
   Opens as a page rather than a widget: the masthead takes the screen,
   the composer is the primary call to action where a form would sit.
   Type the first message and the foyer lifts away — from then on it is
   Column, which is why `talking` is all it takes. */
.layout-foyer:not(.talking) .hd{
  min-height:min(62vh,520px);
  justify-content:center;
  flex-direction:column;
  text-align:center;
  gap:8px}
.layout-foyer:not(.talking) .hd-avatar{width:84px;height:84px;flex-basis:84px;margin:0 auto}
.layout-foyer:not(.talking) .hd-title{font-size:clamp(30px,7vw,52px)}
.layout-foyer:not(.talking) .hd-blurb{margin-left:auto;margin-right:auto}
.layout-foyer:not(.talking) .chat{display:none}
.layout-foyer:not(.talking) .composer{border-top:0}
.layout-foyer .hd,.layout-foyer .hd-avatar,.layout-foyer .hd-title{
  transition:min-height .3s ease,width .3s ease,height .3s ease,font-size .3s ease}
@media (prefers-reduced-motion:reduce){
  .layout-foyer .hd,.layout-foyer .hd-avatar,.layout-foyer .hd-title{transition:none}
}

/* ── CARD ────────────────────────────────────────────────────────────
   For embedding in a page that already exists. No masthead, no privacy
   footer duplicated from the host, no assumed full-viewport height — it
   fits the box it is given and reports its own height over the
   usher:height handshake that already exists. */
.layout-card{height:auto;min-height:0;padding:0 12px}
.layout-card .hd,
.layout-card .privacy{display:none}
.layout-card .composer{position:static;padding-bottom:8px}
.layout-card .chat{overflow:visible}

/* ── R3.1: the four blocks ────────────────────────────────────────────
   Every rule here is drawn by BOTH app.js and worker/ssr.js against the same
   class names, because the server draws the first screen and the client
   redraws it — a crawler and a visitor must not see two different pages.
   Everything is a token, so a block wears the tenant's brand exactly as the
   card around it does. */

.blk{margin:10px 0 0;display:flex;flex-direction:column;gap:10px}

/* answer — postcode in, yes or no out, in the browser */
.blk-answer form{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end}
.blk-q{flex:1;min-width:150px;display:block;color:var(--muted);font-size:var(--fine-size)}
.blk-q input{
  display:block;width:100%;margin-top:5px;padding:9px 11px;
  font:inherit;font-size:var(--offer-body-size);color:var(--fg);
  background:var(--bg);border:1px solid var(--hairline);border-radius:calc(var(--radius) * .6)}
.blk-q input:focus{outline:2px solid var(--accent);outline-offset:1px}
.blk-go{
  flex:0 0 auto;padding:9px 15px;font:inherit;font-size:var(--offer-body-size);
  cursor:pointer;color:var(--fg);background:var(--raised);
  border:1px solid var(--hairline);border-radius:calc(var(--radius) * .6)}
.blk-go:hover{border-color:var(--accent)}
/* A verdict is type, not a chip: a service-area answer is a sentence the
   operator wrote, and dressing it as an alert makes a "no" read as an error. */
.blk-out{flex:1 0 100%;margin:2px 0 0!important;font-size:var(--offer-body-size)}
.blk-out[data-v="yes"]{color:var(--good)}
.blk-out[data-v="no"]{color:var(--muted)}

/* live — open or closed right now */
.blk-state{
  margin:0!important;font-weight:650;font-size:var(--offer-body-size);
  display:flex;align-items:center;gap:7px}
.blk-state::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
.blk-state[data-open="yes"]{color:var(--good)}
.blk-state[data-open="no"]{color:var(--muted)}
.blk-hourswrap summary{
  cursor:pointer;color:var(--accent);font-size:var(--fine-size);
  letter-spacing:.04em;list-style:none}
.blk-hourswrap summary::-webkit-details-marker{display:none}
.blk-hourswrap summary::before{content:"+ "}
.blk-hourswrap[open] summary::before{content:"\2212 "}
.blk-hours{
  display:grid;grid-template-columns:auto 1fr;gap:3px 14px;
  margin:8px 0 0;font-size:var(--offer-body-size)}
.blk-hours dt{color:var(--muted)}
.blk-hours dd{margin:0;color:var(--fg);font-variant-numeric:tabular-nums}
/* The after-hours path is a way out, not the main road. */
.cta.ghost{background:transparent;color:var(--accent);border:1px solid var(--accent)}
.cta.ghost:hover{background:color-mix(in srgb,var(--accent) 12%,transparent);filter:none}

/* proof — read, never clicked. No CTA, by definition of the type. */
.blk-rating{margin:0!important;display:flex;align-items:baseline;gap:6px}
.blk-rating b{
  font-family:var(--font-display);font-size:calc(var(--offer-title-size,1rem) * 1.3);
  font-weight:650;color:var(--fg);letter-spacing:-.01em}
.blk-rating span{color:var(--muted);font-size:var(--offer-body-size)}
.blk-facts{
  display:grid;grid-template-columns:auto 1fr;gap:6px 14px;
  margin:0;font-size:var(--offer-body-size)}
.blk-facts dt{color:var(--muted)}
.blk-facts dd{margin:0;color:var(--fg)}
.blk-note{display:block;color:var(--muted);font-size:var(--fine-size);margin-top:2px}
@media(max-width:420px){
  .blk-facts,.blk-hours{grid-template-columns:1fr;gap:2px}
  .blk-facts dt,.blk-hours dt{margin-top:6px}
}

/* A paused usher: the page is the page, minus the conversation. Quiet and
   plainly worded — this is an operator on holiday, not a fault. */
.paused-note{
  margin:0;padding:14px 16px;border-top:1px solid var(--hairline);
  color:var(--muted);font-size:var(--fine-size);line-height:1.55;text-align:center}
