/* The Listening Post (/labs/listening-post) — desktop.
   Part of the /labs stylesheet, split out of one 3,400-line labs.css.

   THE NUMBER IN THE FILENAME IS LOAD-BEARING. stylesheet_link_tag :app
   collects every stylesheet under app/assets and serves them sorted by
   path, so the ordinal prefix is the only thing holding the cascade order
   these rules were written in — and several classes are shared across labs
   (.lp-head, .lab-entry, .lab-block, .dt-table). Renaming a file without
   renumbering it, or inserting one out of sequence, reorders the cascade
   silently.

   Nothing in this comment may contain the two characters that close a CSS
   comment. The first version of it wrote a shell glob here, which closed
   the comment early; the leaked text then parsed as a selector and ate the
   first real rule in all nine files. There is a test for that now.
   ============================================================ */

/* ---- Listening Post ----
   Scoped styles for the labs pages. Reuses design tokens (--ink, --line,
   --font-serif, etc.) defined globally in app/assets/tailwind/application.css. */

/* Top from --page-top (application.css), which the hero and /labs also read, so the three cannot
   drift apart again. The bottom stays local: it separates this head from whatever the page puts
   under it, which differs per lab.
   Below 680px neither applies — labs_01_composer_mobile.css's `body.lp-demo .lp-head` replaces
   both with 26px/14px to fit the empty state in one viewport, and being two classes deep it also
   beats labs_09's `.fd-head`. That override is already tighter than this token's floor, so mobile
   was never part of the gap this token was introduced to close. */
.lp-head {
  padding-block: var(--page-top) clamp(28px, 4vw, 40px);
  text-align: center;
}

/* page title — serif "human voice" */
.lp-title {
  font-family: var(--font-sans);
  font-weight: 300;
  font-size: clamp(32px, 5.2vw, 54px);
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-top: 18px;
}
.lp-subtitle {
  font-family: var(--font-sans);
  color: var(--ink-muted);
  font-size: clamp(16px, 1.6vw, 18px);
  max-width: 56ch;
  margin: 18px auto 0;
}

.lp-body {
  padding-bottom: clamp(32px, 4vw, 48px);   /* breathing room above the footer band */
}

/* ---- conversation layout ----
   Initial state: the band + composer sit near the top (as authored). Once the
   first question is asked, `.is-conversing` on the controller root hides the
   band, reveals the scrolling transcript, and docks the composer to the
   bottom of the viewport (sticky) so follow-ups read like a chat. */
.lp-transcript:empty { display: none; }

.lp-turn + .lp-turn { border-top: 1px solid var(--line-soft); }
.lp-transcript .lp-turn:first-child .lp-answer-block { margin-top: 0; }

/* Composer dock: normal flow initially; sticky to the screen bottom while conversing. */
.lp-composer-dock { margin-top: 28px; }

/* ---- the corpus line ----
   The data voice, at the measure the composer sits at, directly above it: three
   figures and the word `synthetic`, which is the honest half. Mono because this is
   measurement, which is what that face means everywhere else on the site.
   The figures take --ink and the prose --ink-muted, so the numbers read first at
   12.5px without a second size entering the page. */
.lp-corpus {
  max-width: 760px;
  margin: 28px auto 0;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.5;
}
.lp-corpus-link {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 4px;
  color: var(--ink-muted);
  text-decoration: none;
  border-bottom: 0;
}
.lp-corpus-link b { font-weight: 500; color: var(--ink); }
/* The middot is a separator, not a word: --ink-faint holds it back from the figures
   it divides, and its own margins are what carry the spacing at this size. */
.lp-corpus-sep { color: var(--ink-faint); margin-inline: 6px; }
.lp-corpus-link svg { flex: 0 0 auto; margin-left: 2px; transition: transform .18s ease-out; }
/* --accent is this page's stage ink (clay). It is the one hue in the empty state,
   and it arrives on intent rather than at rest. */
.lp-corpus-link:hover { color: var(--accent); }
.lp-corpus-link:hover b { color: var(--accent); }
.lp-corpus-link:hover svg { transform: translateX(3px); }
.is-conversing .lp-corpus { display: none; }
/* It sits outside the dock, so the dock's own 28px would double the gap between a
   line and the field it describes. 10px is close enough to read as one object. */
.lp-corpus + .lp-composer-dock { margin-top: 10px; }

/* THE EMPTY STATE IS NOT VERTICALLY CENTRED, AND IT WAS FOR ONE AFTERNOON.
   The composer was flex-centred in the space between the band and the footer. Two
   measurements killed it. It does not fire where it was meant to: at 1440x757 the
   composer already sits 28px under the band with 49px to the footer and the page
   still scrolls 18px, so free space exists only above a ~894px viewport — taller
   than a 13" laptop, taller than a 1512x982 window. And where it does fire it
   centres a void, because the empty state had nothing in it: the corpus line and
   the preset chips below are what that space actually wanted. Reverted rather than
   tuned. Anything that reintroduces `display: flex` on main.lp-page must also
   restore `.dt-stats-row { width: 100% }`'s reason for existing (site_03_hero.css)
   — an auto cross-axis margin beats `align-items: stretch`, so every .wrap on
   these pages shrinks to its own content the moment that rule returns. */

/* "Start over" lives inside the composer bar next to the segment menu; it uses the
   .lp-menu-trigger look and only appears once a conversation is underway. The
   two-class selector beats .lp-menu-trigger's own `display` so it stays hidden
   until `.is-conversing`. */
.lp-composer-menus .lp-startover { display: none; }

.is-conversing .lp-head { display: none; }
.is-conversing .lp-startover { display: inline-flex; }
.is-conversing .lp-transcript {
  padding-top: clamp(24px, 5vw, 48px);
  /* The answer reads top-down: it starts just under the header and grows downward,
     scrolling only once it fills the space above the composer (auto-follow in the JS
     controller keeps the newest line in view).

     BOTH the dock AND the footer have to be reserved here, and this reserved only
     the dock until 2026-09-03. While conversing, .ft-band is pinned at bottom 0 and
     the dock is pinned above it at `bottom: var(--lp-footer-h)` — so between them
     they occlude footer-h + dock-h of the viewport, and both are out of flow, so
     nothing in the document reserves either. With only the dock reserved, the last
     ~footer-h of the answer sat behind the pinned footer and could not be scrolled
     into view AT ANY SCROLL POSITION: the page simply ended mid-sentence. The
     comment that used to sit here argued --lp-footer-h was "already consumed by the
     dock's own `bottom`" — it is, for POSITIONING the dock, which says nothing about
     the content underneath the footer. It is not double counting.

     The mobile rule (labs_01) reserves the dock alone, correctly: ≤680px hides
     .ft-band outright, so there is no footer to clear. */
  padding-bottom: calc(var(--lp-dock-h, 168px) + var(--lp-footer-h, 76px) + 24px);
}
.is-conversing .lp-body { padding-bottom: 0; }

/* The composer and the global footer are both pinned to the bottom of the viewport
   while a conversation is underway, so neither drifts as the answer streams in. The
   class lives on <body> because the footer is outside the controller's element; the
   controller keeps --lp-dock-h / --lp-footer-h up to date.

   .ft-band, not the <footer> itself: footer.ft sits inside .wrap, so pinning it
   directly would take it out of the column and strand its four labels against the
   viewport edges. The band is the full-width element and pinning it keeps the
   measure. (This targeted footer.cx-footer until 2026-09-02, a class that stopped
   existing at Prism II — so the footer was not pinned at all and --lp-footer-h was
   never written, leaving the dock floating on the literal 76px fallback below.) */
body.lp-conversing .lp-composer-dock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--lp-footer-h, 76px);
  z-index: 30;
  padding: 16px var(--gutter) 20px;
  /* opaque base with a short top fade so transcript text scrolls cleanly under it */
  background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, var(--bg) 22px, var(--bg) 100%);
}
/* The Other labs row (labs/shared/_other_labs) sits outside the controller root, so
   it is hidden on the body class, not .is-conversing: under a running conversation it
   would scroll up beneath the pinned composer and footer. */
body.lp-conversing .labs-rel { display: none; }

body.lp-conversing .ft-band {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: var(--bg);
  box-shadow: 0 1px 0 1px var(--bg);   /* cover any sub-pixel transcript sliver at the very edge */
}

/* ---- composer (chat-style input) ----
   A single large input surface: a growing textarea over a toolbar carrying the
   two dropdown menus and the send control. Monochrome, hairline-bordered — the
   only concession to the "chat input" idiom is a soft corner radius. */
.lp-composer {
  max-width: 760px;
  margin: 0 auto;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 16px;
  transition: border-color 0.2s;
}
.lp-composer:focus-within { border-color: var(--ink); }

.lp-composer-input {
  display: block;
  width: 100%;
  border: none;
  background: transparent;
  resize: none;
  overflow-y: auto;
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
  padding: 20px 20px 6px;
  min-height: 58px;
  max-height: 200px;
}
.lp-composer-input::placeholder { color: var(--ink-faint); }
/* The field kills its own ring (the parent's :focus-within border is the visible
   state) — but only for a POINTER focus. A keyboard arrival gets the ring back,
   because a 1px border shift is the weakest signal on the page and this is the
   page's primary target. */
.lp-composer-input:focus { outline: none; }
.lp-composer-input:focus-visible {
  outline: 2px solid var(--fill-primary);
  outline-offset: 3px;
  border-radius: 4px;
}

.lp-composer-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px 10px;
}
.lp-composer-menus { display: flex; flex-wrap: wrap; gap: 8px; }

.lp-composer-send {
  margin-left: auto;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  /* WAS #0B0B0C, A LITERAL. Against the dark ground (--bg #14120F) that measured
     ~1.05:1, so the button had no shape at all in dark mode and only its white
     arrow floated — WCAG 1.4.11 asks 3:1 of a control's boundary. --fill-human is
     the warm action fill, defined in both themes with --on-fill as its partner, and
     it gives the composer the one piece of the spectrum it never carried. */
  background: var(--fill-human);
  color: var(--on-fill);
  border: none;
  border-radius: 9999px;
  cursor: pointer;
  transition: background 0.2s, opacity 0.2s;
}
.lp-composer-send:hover { background: var(--fill-human-hi); }
.lp-composer-send:disabled { opacity: 0.4; cursor: default; }

/* SEND AND STOP ARE THE SAME BUTTON. It stays full strength while a stream runs —
   it is the live control, not a dimmed one — and only the glyph changes. Both
   icons are in the markup and CSS picks; swapping innerHTML would lose the
   element every time and there is nothing to animate between two shapes. */
.lp-stop-ico { display: none; }
.lp-composer-send.is-busy .lp-send-ico { display: none; }
.lp-composer-send.is-busy .lp-stop-ico { display: block; }

/* ---- composer dropdown menus ---- */
.lp-menu { position: relative; }
.lp-menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--ink-muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 13px;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
}
.lp-menu-trigger:hover { border-color: var(--ink); }
/* WAS color-mix(in srgb, var(--ink) 3%, transparent) — pure black at 3%, which a #14120F ground swallows whole,
   so the open trigger had no fill at all in dark mode. --bg2 is the surface token
   and is declared in both themes, so the wash inverts with the page instead of
   always darkening it. */
.lp-menu-trigger[aria-expanded="true"] {
  border-color: var(--ink);
  background: var(--bg2);
}
.lp-menu-ico { flex: 0 0 auto; color: var(--ink-faint); }

.lp-menu-panel {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 30;
  width: max-content;
  min-width: min(360px, 84vw);
  max-width: min(560px, 92vw);
  background: var(--bg);
  border: 1px solid var(--line);
  /* The panel had no shadow and floated on a hairline, so it read as part of the
     page rather than above it — most visible where it opens across the footer's
     top edge. Same value .lp-tip already uses (labs_01), which is the only other
     thing on this page that leaves the surface. */
  box-shadow: 0 8px 28px rgba(11, 11, 12, 0.12);
  border-radius: 12px;
  overflow: hidden;
  /* Closed is the resting state; .is-open is added/removed a frame apart from
     the hidden attribute by whichever controller owns this panel (listening_post,
     home_ask, preset_menu), so there's always a frame to transition from. Anchored
     to the trigger it grows from: this panel opens downward by default, so the
     origin sits at its own top edge. */
  transform-origin: top left;
  opacity: 0;
  transition: opacity 120ms ease;
}
.lp-menu-panel.is-open { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .lp-menu-panel {
    transform: scale(0.97);
    transition: opacity 140ms cubic-bezier(0.2, 0, 0.2, 1), transform 140ms cubic-bezier(0.2, 0, 0.2, 1);
  }
  .lp-menu-panel.is-open { transform: scale(1); }
}
.lp-menu-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-soft);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.lp-menu-close {
  /* It measured 16x18. The glyph stays that size and the target does not: a
     negative-inset ::after carries the 44px without moving the header's baseline,
     the same trick labs_01 already uses on the mobile send button. */
  position: relative;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 17px;
  line-height: 1;
  color: var(--ink-faint);
  padding: 0 2px;
  transition: color 0.2s;
}
.lp-menu-close::after { content: ""; position: absolute; inset: -13px -14px; }
.lp-menu-close:hover { color: var(--ink); }
.lp-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-top: 1px solid var(--line-soft);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.4;
  color: var(--ink);
  padding: 13px 16px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s;
}
.lp-menu-item:first-of-type { border-top: none; }
/* The applied segment, visible. A tick rather than a fill: the row already has a
   hover wash and two washes on one row read as two states. --accent is this page's
   stage ink, which is a text partner in both themes. */
.lp-menu-item[aria-checked="true"] { color: var(--accent); font-weight: 500; }
.lp-menu-item[aria-checked="true"]::after {
  content: "";
  float: right;
  width: 12px;
  height: 7px;
  margin-top: 5px;
  border: 1.7px solid currentColor;
  border-top: 0;
  border-right: 0;
  transform: rotate(-45deg);
}
/* Same fix as the trigger's open state: the hover wash was pure black at 4%, so the
   preset list — the page's one recognition affordance — had no hover feedback at all
   in dark mode. :focus-visible joins it, because a keyboard walking the menu was
   getting the ring and no row highlight. */
.lp-menu-item:hover,
.lp-menu-item:focus-visible { background: var(--bg2); }

@media (max-width: 680px) {
  .lp-menu-panel { width: auto; min-width: min(320px, 84vw); max-width: 92vw; }
  .lp-menu-item { white-space: normal; }
}

/* ---- the marketing band's composer (.home-ask) ----
   Everything below is scoped to .home-ask because it wires up
   home_ask_controller.js's fitPanel(), which is homepage-only — the lab's
   composer docks to the bottom of the viewport, so the whole page is its
   headroom and there is nothing to measure. The LOOK is still the lab's; these
   are placement rules for a composer sitting in a page rather than in a chat
   column.

   THE CONTROLLER WAS WRITING TO NOTHING. fitPanel() sets --lp-menu-max and
   toggles .is-below on every open, and until now no rule in any stylesheet read
   either one — its own comment describes a "CSS caps it at min(46vh, 340px)"
   rule that was never written. So the panel always opened downward off the base
   rule, at whatever height its contents wanted: on a phone the five-item
   Common questions list ran past the band's lower edge onto the section below,
   and on a short viewport it had no ceiling at all. Both failures render
   perfectly and raise nothing. The two rules below are the other half of that
   mechanism; changing either means reading the controller first. */
.home-ask .lp-menu-panel {
  max-height: var(--lp-menu-max, none);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* fitPanel measures the room above and below the composer and opens into the
   roomier side, marking the downward case. Upward is therefore the default: an
   unmarked panel is one the controller has not measured, and above the field is
   where a composer low on the page needs it. */
/* Opens upward here (see fitPanel() above), so the panel should visibly grow
   from the trigger it hangs off — the bottom edge — rather than keep the
   downward-opening default's top-left origin. */
.home-ask .lp-menu-panel:not(.is-below) { top: auto; bottom: calc(100% + 8px); transform-origin: bottom left; }

/* Below 560px the panel spans the composer instead of the trigger it hangs off.
   The segment trigger sits mid-bar, so a panel anchored to it and sized by its
   own contents overflowed the composer's right edge and the band's ground with
   it (measured at 390px: a 350px panel against a 342px composer). Neutralising
   .lp-menu's positioning hands the panel to the bar, which is the composer's
   own width. Same shape as .mb-form's override in labs_05_monday_brief.css. */
@media (max-width: 560px) {
  .home-ask .lp-composer-bar { position: relative; }
  .home-ask .lp-menu { position: static; }
  .home-ask .lp-menu-panel {
    left: 0;
    right: 0;
    width: auto;
    min-width: 0;
    max-width: none;
  }

  /* THE PLACEHOLDER IS THE FIELD'S ONLY INSTRUCTION AND IT WAS BEING CUT IN
     HALF. "Ask the customer-feedback dataset a question…" wraps to two lines
     below ~420px, which needs 77px; the field is authored at 58px with
     overflow-y: auto, so the second line was simply clipped and a visitor never
     learned what the box was for. The partial's own header forbids shortening
     the sentence (an unlabelled chat box on a B2B page reads as a support
     widget), so the field grows instead. Two lines at 16px/1.6 plus the 20px/6px
     padding is 78px. The lab is untouched — its placeholder is four words and
     body.lp-demo pins its own 44px. */
  .home-ask .lp-composer-input { min-height: 78px; padding-inline: 16px; }
}

/* TOUCH TARGETS, ON THE SAME 680px BREAKPOINT THE LAB USES.

   body.lp-demo already raises both of these ("≥44px touch targets even where the
   control looks smaller", labs_01_composer_mobile.css) and expands the send
   button's hit area with a ::after — so the intent is the project's own, it just
   never reached the second surface the composer renders on. On the marketing
   band they stayed at the authored 38px trigger and 40x40 send at every width.

   Desktop keeps the authored sizes: 38px and 40px are correct under a cursor and
   are what the composer was drawn as. This is keyed on width rather than on
   `pointer: coarse` for the reason site_01_chrome.css gives at the footer — a
   desktop browser at a narrow window reports a fine pointer, so the rule would be
   invisible to the person checking it and absent for a reader on a laptop.

   The band is not space-constrained the way the lab's docked bar is, so the send
   button takes a real 44x44 rather than the lab's expanded-hit-area trick.

   NOT A FOOTER RULE. footer.ft's links deliberately do not carry this: the site's
   44px law is scoped to .btn pills — actions — and site_01_chrome.css argues that
   case explicitly, measures the links at 25.5px against WCAG 2.5.8 AA's 24px
   floor, and fixes the real defect there (adjacent rows sharing an edge) with
   separation instead of height. Don't "correct" that from here. */
@media (max-width: 680px) {
  .home-ask .lp-menu-trigger { min-height: 44px; }
  .home-ask .lp-composer-send { width: 44px; height: 44px; }
}

/* ---- suggestion chips ----
   Shown on every width now. Desktop takes three under the composer and keeps the
   full five in the dropdown; labs_01 turns this row into the scrolling strip above
   the docked input below 680px, where all five ride along. */
.lp-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  max-width: 760px;
  margin: 14px auto 0;
}
.is-conversing .lp-chips { display: none; }

@media (min-width: 681px) {
  /* Three is the count a reader scans without reading; the other two are one click
     away and the whole list is still in the menu. */
  .lp-chips .lp-chip:nth-child(n+4) { display: none; }
}
.lp-chip {
  flex: 0 0 auto;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--ink-muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 9px 13px;
  min-height: 40px;
  cursor: pointer;
  transition: border-color 0.2s;
}
.lp-chip:hover { border-color: var(--ink); }
