/*!
 * OSSA WIDGET, the help assistant's front end.
 * ------------------------------------------------------------------
 * Paired with /assets/js/ossa-widget.js, which mounts itself. This file
 * paints two things and nothing else: the closed honeycomb launcher, and
 * the open panel that reads like Messages on a Mac.
 *
 * Load order on any page:
 *   /css/styles.css                      <- brand tokens live here
 *   /assets/css/dashboard-theme.css      <- authenticated pages only, remaps
 *                                           the same token NAMES for dark
 *   /assets/css/ossa-widget.css          <- this file
 *
 * WHY THERE IS NO DARK BLOCK IN THIS FILE.
 * Almost none of it needs one. dashboard-theme.css remaps the VALUES of
 * --bone-cream, --bone-white, --bone-black, --steel and --line under
 * :root[data-mz-theme="dark"], so a rule written against those token names
 * flips for free and can never drift from the rest of the dashboard. The
 * places that genuinely cannot follow a token are called out at the site
 * of the decision below: the ink on the teal fill, the two halo stops that
 * lift on a black ground, the panel shadow, and the hairline that separates
 * Ossa's bubble from the panel on dark.
 *
 * Rules honored:
 *   - NO NEW BRAND TOKENS. This file declares no custom property at all.
 *     Every colour is read from a token that already ships. The only
 *     literals are var() fallbacks, and each one is byte identical to the
 *     value css/styles.css or dashboard-theme.css carries for that token,
 *     which is what tests/one-brand-token-source.test.mjs part C checks.
 *     The teal token NAMES are historical. --marrow-red is #2C685E.
 *   - Vanilla CSS. No build step, no framework, no library.
 *   - prefers-reduced-motion stills every animation here, including the
 *     launcher glow and the typing dots, with a static fallback rather
 *     than a frozen frame.
 *   - No em or en dashes anywhere in this file.
 */

/* =====================================================================
   1. THE LAUNCHER, CLOSED STATE
   ---------------------------------------------------------------------
   A CELL OF BONE WITH THE MARROW LIT INSIDE IT.

   The metaphor is the brand's own and it is pushed literally here. The
   disc is the marrow, painted in the accent so the launcher reads as teal
   from across the room. The honeycomb drawn over it is trabecular bone, in
   the constant bone ink an accent fill always takes. The light comes from
   the centre cell and falls off through the comb and out past the rim as
   a soft halo on the page, which is the glow Edwin asked for. Nothing here
   is a drop shadow: a shadow under a circle reads as a button on paper,
   and this is meant to read as a small lit object.

   Layers, painted in this order by honeycombMarkup() in ossa-widget.js:
     halo     the bloom, a radial fade of the accent that extends PAST the
              disc and breathes on a nine second cycle
     body     the marrow, a solid disc of the brand accent
     light    a radial fade of the lifted accent centred on the marrow, so
              the disc is brightest where the light source is
     wall     the cortical rim, a hairline of bone at the disc's edge
     lattice  the six bone cells of the mark
     core     the centre cell, the marrow itself, filled with the lifted
              accent so it is the one cell that is visibly lit
     spark    a small bone hot spot on the core, the point the light leaves

   Every colour below is a token that already ships. The lifted accent
   (--marrow-ember-text) is the only teal in the system brighter than the
   body, and a light has to be brighter than the thing it lights, which is
   why the inner glow and the core reach for it rather than for the glow
   token, which is five sRGB steps from the body and invisible on it.
   ===================================================================== */

/*
 * z-index 900 is the SAME rule the view switcher uses, for the same
 * reason it gives: above ordinary page content, below a modal scrim. A
 * floating control that paints over a confirm dialog is how somebody taps
 * the wrong thing. Both controls sitting on one layer also means they
 * stack predictably against each other rather than by accident.
 *
 * The value is duplicated in ossa-widget.js because the JS writes the
 * inline position on the host it creates. tests/ossa-widget.test.mjs
 * reads BOTH files and fails if they disagree, so the duplicate cannot
 * rot in silence.
 *
 * BOTTOM RIGHT, ON PURPOSE. Edwin offered bottom centre as the first
 * option. The corner won: a centred control sits on the iOS home indicator
 * and on Safari's bottom bar, it covers content where a corner covers
 * margin, and positionLauncher() already measures what else is parked in
 * this corner and climbs above it. The safe area inset is folded in here
 * AND in the inline write the JS makes, because the inline value wins.
 */
.ossa-launcher {
  position: fixed;
  right: 20px;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  z-index: 900;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--bone-black, #0A0908);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}

/* The body is the accent, so the ring has to be the page's ink rather than
   the accent, or it vanishes into the halo. Same pairing .ossa-handoff uses
   for an accent filled control. */
.ossa-launcher:focus-visible {
  outline: 2px solid var(--bone-black, #0A0908);
  outline-offset: 4px;
  border-radius: 50%;
}

.ossa-launcher[hidden] { display: none !important; }

.ossa-hive {
  width: 56px;
  height: 56px;
  display: block;
  overflow: visible;
  transform-origin: 50% 50%;
  transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ossa-launcher:hover  .ossa-hive { transform: scale(1.06); }
.ossa-launcher:active .ossa-hive { transform: scale(0.97); transition-duration: 80ms; }

/* THE MARROW. The one thing that makes the launcher read as teal, in both
   themes, because --marrow-red is the accent and it is not remapped. */
.ossa-hive-body { fill: var(--marrow-red, #2C685E); }

/* THE LIGHT INSIDE. A fade of the lifted accent centred on the marrow, so
   the disc is brightest at its source and settles to the body at the rim. */
.ossa-hive-core-in  { stop-color: var(--marrow-ember-text, #53B2A2); stop-opacity: 0.72; }
.ossa-hive-core-mid { stop-color: var(--marrow-ember-text, #53B2A2); stop-opacity: 0.28; }
.ossa-hive-core-out { stop-color: var(--marrow-ember-text, #53B2A2); stop-opacity: 0; }

/* THE BONE. The cortical rim and the six ring cells take the constant light
   ink for an accent fill. It is deliberately NOT --bone-black, which flips
   to bone on dark and would put near black lines on the teal on light.
   --mzv-onfill is the same value on both themes, which is the point. */
.ossa-hive-wall {
  fill: none;
  stroke: var(--mzv-onfill, #FBF8F2);
  stroke-width: 0.9;
  opacity: 0.34;
}

.ossa-hive-lattice polygon {
  fill: none;
  stroke: var(--mzv-onfill, #FBF8F2);
  stroke-width: 1.3;
  stroke-linejoin: round;
  opacity: 0.78;
  transition: opacity 220ms ease;
}
.ossa-launcher:hover .ossa-hive-lattice polygon { opacity: 1; }

/* THE MARROW CELL. Filled with the lifted accent and edged in bone, so it
   is the one visibly lit cell in the comb and the place the light leaves. */
.ossa-hive-core {
  fill: var(--marrow-ember-text, #53B2A2);
  stroke: var(--mzv-onfill, #FBF8F2);
  stroke-width: 0.9;
  stroke-linejoin: round;
  opacity: 0.96;
}

/* The hot spot on the core. Bone light at the exact centre, gone by the
   cell's edge, which is what turns a lighter cell into a lit one.

   HELD LOW ON PURPOSE. At 0.62 this read as a specular highlight, the white
   dot a glossy app icon wears, and it was the single thing that made the
   launcher look like a button rather than a lit object. At 0.3 it is warmth
   on the cell, not a reflection on it. */
.ossa-hive-spark-in  { stop-color: var(--mzv-onfill, #FBF8F2); stop-opacity: 0.3; }
.ossa-hive-spark-out { stop-color: var(--mzv-onfill, #FBF8F2); stop-opacity: 0; }

/*
 * THE HALO. LIT FROM WITHIN, NOT DROPPED BEHIND.
 *
 * A box-shadow spreads outward from an edge and always reads as a shadow
 * wearing a colour. This is a radial gradient painted UNDER the disc and
 * larger than it, so the only part that shows is the part past the rim,
 * and that part fades from the disc's edge to nothing. The eye reads it as
 * light leaving the object. The stops are styled here rather than in the
 * SVG markup so the colour stays a token and never becomes a literal in a
 * JS string. The inner stop sits under the disc and is only there so the
 * fade has a smooth start.
 */
/*
 * ON CREAM, A GLOW IS A STAIN. Light only reads as light against a ground
 * darker than itself. On the bone cream the teal halo is darker than the
 * page, so what the eye sees past the rim is not a bloom but a tint, and at
 * the old 0.34 rim stop it was a green smudge the disc sat in. These values
 * are set for the light ground: enough tint to seat the disc on the page,
 * because a flat teal circle on cream looks pasted on, and no more. The
 * dark stamp below is where the halo is allowed to be a halo.
 */
.ossa-hive-stop-in  { stop-color: var(--marrow-red-glow, #2F6F65); stop-opacity: 0.3; }
.ossa-hive-stop-mid { stop-color: var(--marrow-red-glow, #2F6F65); stop-opacity: 0.22; }
.ossa-hive-stop-far { stop-color: var(--marrow-red-glow, #2F6F65); stop-opacity: 0.06; }
.ossa-hive-stop-out { stop-color: var(--marrow-red-glow, #2F6F65); stop-opacity: 0; }

/* On black the same gradient IS light leaving the object, so the two stops
   that show are lifted to where a glow needs to be to read as one. */
:root[data-mz-theme="dark"] .ossa-hive-stop-mid { stop-opacity: 0.46; }
:root[data-mz-theme="dark"] .ossa-hive-stop-far { stop-opacity: 0.16; }

.ossa-hive-bloom {
  opacity: 0.75;
  transform-origin: 32px 32px;
  /* NINE SECONDS, and the slowness is the point. Edwin asked for a glow that
     breathes very slowly or not at all. Anything under about six seconds
     stops being ambient and starts being a notification nobody dismissed. */
  animation: ossa-breathe 9s ease-in-out infinite;
}

/*
 * THE BREATH IS BRIGHTNESS ONLY. It used to scale as well, 0.97 to 1.04, and
 * a thing that changes SIZE in the corner of the eye is a thing the eye
 * keeps checking on, which is the fidget. A light that slowly brightens and
 * dims is what a room reads as ambient. The swing is narrow for the same
 * reason: 0.72 to 0.9 is a breath, 0.62 to 0.92 was a pulse.
 */
@keyframes ossa-breathe {
  0%, 100% { opacity: 0.72; }
  50%      { opacity: 0.9; }
}

/* On hover the light comes up: the halo holds at full, the inner glow
   brightens. The keyframes own the bloom's opacity while it animates, so
   hover pauses the breath at its brightest rather than fighting it. The
   same wake state fires while the introduction card is on screen, so the
   card and the mark it points at are visibly one event. */
.ossa-launcher:hover .ossa-hive-bloom,
.ossa-launcher[data-ossa-intro="1"] .ossa-hive-bloom { animation-play-state: paused; opacity: 1; }
.ossa-launcher:hover .ossa-hive-core-in,
.ossa-launcher[data-ossa-intro="1"] .ossa-hive-core-in { stop-opacity: 0.9; }
.ossa-launcher[data-ossa-intro="1"] .ossa-hive-lattice polygon { opacity: 1; }

/* The one word beneath the mark, on hover and on focus only. Sits just
   under the halo's reach. */
.ossa-launcher-word {
  position: absolute;
  bottom: -16px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--mono, 'JetBrains Mono', 'SF Mono', Menlo, monospace);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--steel, #475569);
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
}
.ossa-launcher:hover .ossa-launcher-word,
.ossa-launcher:focus-visible .ossa-launcher-word { opacity: 1; }

/*
 * THE UNREAD MARK IS A TEAL DOT, NOT A RED BADGE.
 * Red is retired as Marrow branding and survives only as meaning, so a red
 * dot on this launcher would read as "something is wrong" when the truth is
 * "your answer arrived". No count either: a number invites counting, and
 * there is only ever one conversation here.
 *
 * It sits in the button's corner, mostly OFF the disc, and wears a ring of
 * the constant bone ink so a teal dot never has to be found on a teal body
 * and reads on both page grounds.
 */
.ossa-launcher-dot {
  position: absolute;
  top: 1px;
  right: 1px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--marrow-red-glow, #2F6F65);
  box-shadow: 0 0 0 2px var(--mzv-onfill, #FBF8F2);
  display: none;
}
.ossa-launcher[data-ossa-unread="1"] .ossa-launcher-dot { display: block; }

/* COLLAPSED. Close and minimize both land here: a pill that keeps her on
   the page as a small teal coin beside her name. The halo is dropped
   because a glow bleeding over a pill's own ground reads as a smudge.

   44 tall, not 40. The pill is the thing a person taps to get her back, on
   a phone more often than not, and 40 is under the touch target floor. The
   JS measures the pill at 40 when it climbs past a corner blocker; that
   number is informational and a 4px difference cannot cause an overlap, but
   it should read 44 to match. */
.ossa-launcher--pill {
  width: auto;
  height: 44px;
  padding: 0 15px 0 9px;
  gap: 9px;
  border-radius: 22px;
  background: var(--bone-white, #FBF8F2);
  border: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  transition: border-color 160ms ease;
}
.ossa-launcher--pill .ossa-hive { width: 26px; height: 26px; }
.ossa-launcher--pill .ossa-hive-bloom { display: none; }
.ossa-launcher--pill:hover .ossa-hive { transform: none; }
.ossa-launcher--pill:hover { border-color: var(--marrow-red, #2C685E); }
.ossa-launcher--pill .ossa-launcher-word {
  position: static;
  transform: none;
  opacity: 1;
  font-size: 11px;
  color: var(--bone-black, #0A0908);
}
/* The pill has its own edge, so its focus ring goes back to the accent. */
.ossa-launcher--pill:focus-visible {
  outline: 2px solid var(--marrow-red, #2C685E);
  outline-offset: 2px;
  border-radius: 22px;
}

/*
 * AT 26PX THE COMB IS REDRAWN, NOT SHRUNK.
 *
 * The full mark is tuned for 56px. Scaled to 26 the six cells become a
 * 0.5px grey fuzz, the cortical rim becomes noise at 0.37px, and the spark
 * is a smear, which is exactly what made the coin in the pill look like a
 * broken copy of the launcher rather than a deliberate small version of it.
 * So the two small instances, the pill and the panel header, drop the rim
 * and the spark, thicken the cells to where they resolve, and keep the lit
 * core. Same object, drawn for its size, the way a favicon is drawn for its.
 */
.ossa-launcher--pill .ossa-hive-wall,
.ossa-head .ossa-hive-wall,
.ossa-launcher--pill .ossa-hive-spark,
.ossa-head .ossa-hive-spark { display: none; }
.ossa-launcher--pill .ossa-hive-lattice polygon,
.ossa-head .ossa-hive-lattice polygon { stroke-width: 2.1; opacity: 0.9; }
.ossa-launcher--pill .ossa-hive-core,
.ossa-head .ossa-hive-core { stroke-width: 1.6; }

/* =====================================================================
   1b. THE INTRODUCTION
   ---------------------------------------------------------------------
   SHE SAYS HELLO ONCE, IN HER OWN BUBBLE, AND THEN NEVER AGAIN.

   The person who has never seen her does not know the honeycomb is help.
   The one thing that solves that without becoming furniture is the thing a
   person would do: she introduces herself. One small bubble rises beside
   the mark, in the same shape her bubbles take inside the panel, with a
   caret pointing at the disc so the sentence "I'm right here" is literally
   true. It arrives after the page has settled, not on load. It leaves on
   its own. It never takes focus, never covers more than the margin the
   launcher already owns, and it fires ONCE per person per browser, which
   is the whole reason it can be allowed to exist at all. The JS writes the
   storage key the moment it is shown; there is no second showing, ever.

   Tapping the card opens her, because the card is an invitation and the
   only sensible answer to an invitation is to accept it. Anything else,
   a click elsewhere, a real scroll, Escape, or fourteen seconds, lets it
   go. There is no close control on it: an X is what a cookie banner wears,
   and a thing that leaves on its own does not need one.

   It is aria-hidden. The launcher already carries the full accessible
   name, "Open Ossa, the Marrow help assistant", which is this same
   introduction delivered the way assistive tech expects, so announcing the
   card would tell a screen reader user something twice while interrupting
   whatever they were reading.
   ===================================================================== */
.ossa-intro {
  position: fixed;
  /* Beside the disc, 10px clear of it: 20 margin + 56 disc + 10. */
  right: 86px;
  /* The JS overwrites this with launcherBottomCss(bottom, 8), so the card
     rides up with the launcher when it climbs past a corner blocker. The
     value here is the resting position for the moment before that runs. */
  bottom: calc(28px + env(safe-area-inset-bottom, 0px));
  z-index: 900;
  max-width: min(300px, calc(100vw - 102px));
  padding: 10px 14px 11px;
  border-radius: 18px;
  /* The caret sits on this corner, and a caret on a full radius corner
     lands on the curve. */
  border-bottom-right-radius: 10px;
  background: var(--bone-white, #FBF8F2);
  border: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  /* The same single floating shadow the panel wears, light theme only. */
  box-shadow: 0 18px 48px rgba(10, 9, 8, 0.16);
  color: var(--bone-black, #0A0908);
  font-family: var(--sans, 'Inter', -apple-system, system-ui, sans-serif);
  font-size: 14px;
  line-height: 1.4;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  animation: ossa-intro-in 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
:root[data-mz-theme="dark"] .ossa-intro { box-shadow: none; }
.ossa-intro[hidden] { display: none !important; }
.ossa-intro[data-ossa-leaving="1"] {
  animation: ossa-intro-out 220ms ease forwards;
  pointer-events: none;
}

/* Arrives from the disc's side, six pixels, and settles. Leaves by fading
   only, because a thing that slides away draws the eye on its way out. */
@keyframes ossa-intro-in {
  from { opacity: 0; transform: translateX(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes ossa-intro-out {
  to { opacity: 0; }
}

/* THE CARET. A square in the card's own fill, turned so one corner points
   at the disc, with a border on the two edges that show. It is painted
   over the card's right edge so the card border does not run across its
   base. Its vertical seat is set so the point lands on the disc's centre:
   the card's bottom edge sits 8px above the launcher's, the disc centre
   is 28px above the launcher's, so the centre is 20px up the card's edge. */
.ossa-intro::after {
  content: "";
  position: absolute;
  right: -6px;
  bottom: 15px;
  width: 10px;
  height: 10px;
  background: var(--bone-white, #FBF8F2);
  border-top: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  border-right: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  transform: rotate(45deg);
}

/* The eyebrow says WHAT she is in the site's own eyebrow voice, so the
   sentence under it can spend its words on what to do with her. Same
   metrics as .ossa-status in the panel header. */
.ossa-intro-eyebrow {
  display: block;
  margin-bottom: 3px;
  font-family: var(--mono, 'JetBrains Mono', 'SF Mono', Menlo, monospace);
  font-size: 10px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--steel, #475569);
}
.ossa-intro p { margin: 0; }

/* =====================================================================
   1c. THE LOGO IS ALSO A DOOR
   ---------------------------------------------------------------------
   The launcher and the Marrow mark are the same drawing at two sizes, cell
   for cell, and clicking the mark in the nav opens her. The JS marks the
   element it wires with data-ossa-open and this block is everything a
   person is given to notice that: the cursor, a hover that is the same
   wake the launcher has (the mark lifts, the marrow cell brightens to the
   lifted accent), a pressed state, and a focus ring. Nothing is added to
   the mark at rest. No badge, no caption, no underline, no pulse: a logo
   that advertises a second job stops being a logo.

   The centre cell is targeted by its fill attribute because it is the one
   <use> in the shipped mark that carries one; the six ring cells inherit
   their stroke from the group. A stylesheet rule beats a presentation
   attribute, which is why this works without touching the SVG.
   ===================================================================== */
[data-ossa-open] {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
button[data-ossa-open] {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  display: inline-flex;
  align-items: center;
  line-height: 0;
}
/* The attribute may sit on a wrapper (a button around the mark) or on the
   svg itself (the JS wiring the mark inside an existing home link), and
   both shapes get the same wake. */
[data-ossa-open] svg,
svg[data-ossa-open] {
  transform-origin: 50% 50%;
  transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
[data-ossa-open] use[fill] { transition: fill 220ms ease; }
[data-ossa-open]:hover svg,
svg[data-ossa-open]:hover { transform: scale(1.06); }
[data-ossa-open]:hover use[fill] { fill: var(--marrow-ember-text, #53B2A2); }
[data-ossa-open]:active svg,
svg[data-ossa-open]:active { transform: scale(0.97); transition-duration: 80ms; }
[data-ossa-open]:focus-visible {
  outline: 2px solid var(--marrow-red, #2C685E);
  outline-offset: 4px;
  border-radius: 50%;
}

/* =====================================================================
   2. THE PANEL
   ===================================================================== */

.ossa-panel {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 900;
  width: 380px;
  max-width: calc(100vw - 32px);
  height: 560px;
  max-height: calc(100vh - 40px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bone-white, #FBF8F2);
  border: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  border-radius: 16px;
  /* One sanctioned floating shadow, light theme only, mirroring the
     .mz-floating rule in mz-primitives.css. Dark takes depth from the
     raised surface instead, so the shadow is removed below. */
  box-shadow: 0 18px 48px rgba(10, 9, 8, 0.16);
  color: var(--bone-black, #0A0908);
  font-family: var(--sans, 'Inter', -apple-system, system-ui, sans-serif);
  animation: ossa-panel-in 180ms ease;
}
:root[data-mz-theme="dark"] .ossa-panel { box-shadow: none; }

@keyframes ossa-panel-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.ossa-panel[hidden] { display: none !important; }

/* On a phone it is a sheet, because a 380px card floating on a 390px screen
   is a card with nowhere to go. */
@media (max-width: 520px) {
  .ossa-panel {
    right: 0;
    left: 0;
    bottom: 0;
    width: auto;
    max-width: none;
    height: 88vh;
    border-radius: 16px 16px 0 0;
    border-bottom: 0;
  }
  /* The sheet is docked on the bottom edge, so the composer pads for the
     home indicator or the send button sits under a gesture zone. */
  .ossa-composer { padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px)); }
}

/* ---- Header ---- */

.ossa-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 10px 10px 14px;
  border-bottom: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  background: var(--bone-white, #FBF8F2);
}

.ossa-head .ossa-hive { width: 26px; height: 26px; flex: 0 0 auto; }
/* In the header the mark is a badge, not a beacon: the halo stays as a
   still, soft aura and does not breathe. */
.ossa-head .ossa-hive-bloom { animation: none; opacity: 0.55; }
.ossa-head-text { min-width: 0; flex: 1 1 auto; }

.ossa-title {
  margin: 0;
  font-family: var(--display, 'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif);
  font-weight: var(--weight-serif, 400);
  font-size: 15px;
  line-height: 1.1;
  letter-spacing: 0.01em;
}

.ossa-status {
  font-family: var(--mono, 'JetBrains Mono', 'SF Mono', Menlo, monospace);
  font-size: 10px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--steel, #475569);
  display: block;
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ossa-head-actions { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }

.ossa-icon-btn {
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--steel, #475569);
  cursor: pointer;
}
.ossa-icon-btn:hover { background: var(--paper-nested, #EFE9DC); color: var(--bone-black, #0A0908); }
.ossa-icon-btn:focus-visible { outline: 2px solid var(--marrow-red, #2C685E); outline-offset: 1px; }
.ossa-icon-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.ossa-icon-btn[hidden] { display: none !important; }

/* Muted is the default state, so the pressed look has to read as the quiet
   one rather than as the active one. Sound ON is what gets the accent. */
.ossa-icon-btn[aria-pressed="false"] { color: var(--marrow-red, #2C685E); }

/* ---- The transcript ---- */

.ossa-log {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 14px 14px 6px;
  display: flex;
  flex-direction: column;
  /* Messages spaces RUNS apart and keeps bubbles inside a run close. The
     tight value is the default and the run boundary opens it up below. */
  gap: 2px;
  background: var(--bone-white, #FBF8F2);
}

.ossa-row { display: flex; position: relative; }
.ossa-row--user { justify-content: flex-end; }
.ossa-row--ossa { justify-content: flex-start; }
/* First bubble of a new run gets the breathing room. Everything inside a run
   keeps the 2px gap above, which is what makes a run read as one block. */
.ossa-row[data-ossa-pos="first"],
.ossa-row[data-ossa-pos="only"] { margin-top: 10px; }
.ossa-log > .ossa-row:first-child { margin-top: 0; }

/* ---- The bubbles ---- */

.ossa-bubble {
  position: relative;
  max-width: 78%;
  padding: 7px 13px 8px;
  /* The Apple radius. 18px on a 15px/1.35 line is the ratio that makes a one
     line bubble read as a capsule and a five line one still read as a bubble. */
  border-radius: 18px;
  font-size: 15px;
  line-height: 1.35;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* THE USER BUBBLE IS TEAL AND STAYS TEAL, in both themes.
   --marrow-red is the teal #2C685E. The name is historical and the value is
   the brand accent, so this consumes the token rather than restating a hex.
   It is deliberately NOT wrapped in a theme block: a bubble that changes
   colour when somebody flips to dark is a bubble that lost its identity. */
.ossa-bubble--user {
  background: var(--marrow-red, #2C685E);
  /* --mzv-onfill is the constant light ink for an accent fill and it is NOT
     remapped for dark, which is exactly why it is the right token here.
     --bone-white would flip to #171411 on dark and put near black ink on the
     teal at about 2.4 to 1. This pairing measures 6.10 to 1 on both themes,
     recomputed when the parchment sweep moved the accent to #2C685E. */
  color: var(--mzv-onfill, #FBF8F2);
}

/* Ossa is the quiet one. The Messages grey equivalent drawn from the surface
   tokens, so the teal stays the only saturated thing on the screen. The
   hairline is what keeps it separate from the panel on dark, where the page
   ground token is DARKER than the card token rather than lighter. */
.ossa-bubble--ossa {
  background: var(--paper-nested, #EFE9DC);
  border: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  color: var(--bone-black, #0A0908);
}

/*
 * RUN GROUPING, THE MESSAGES RULE.
 * Consecutive bubbles from one sender tighten the corners on THEIR OWN side
 * so the run reads as one block, and only the last bubble of a run carries
 * the tail. The positions are stamped by the JS grouping function, which is
 * the thing under test, so this stylesheet stays a pure consequence of it.
 */
.ossa-row[data-ossa-pos="first"]  .ossa-bubble--user { border-bottom-right-radius: 5px; }
.ossa-row[data-ossa-pos="middle"] .ossa-bubble--user { border-top-right-radius: 5px; border-bottom-right-radius: 5px; }
.ossa-row[data-ossa-pos="last"]   .ossa-bubble--user { border-top-right-radius: 5px; }

.ossa-row[data-ossa-pos="first"]  .ossa-bubble--ossa { border-bottom-left-radius: 5px; }
.ossa-row[data-ossa-pos="middle"] .ossa-bubble--ossa { border-top-left-radius: 5px; border-bottom-left-radius: 5px; }
.ossa-row[data-ossa-pos="last"]   .ossa-bubble--ossa { border-top-left-radius: 5px; }

/*
 * THE TAIL. Two shapes: a curved lobe in the bubble's own fill, then a
 * cut out in the PANEL's fill that carves the hook. It is the same two
 * pseudo element construction Messages draws, and it is why the panel
 * background token appears twice below. Both follow the theme because both
 * are tokens.
 */
.ossa-row[data-ossa-tail="1"] .ossa-bubble::before,
.ossa-row[data-ossa-tail="1"] .ossa-bubble::after {
  content: "";
  position: absolute;
  bottom: 0;
  height: 18px;
  pointer-events: none;
}

.ossa-row[data-ossa-tail="1"] .ossa-bubble--user::before {
  right: -6px;
  width: 18px;
  background: var(--marrow-red, #2C685E);
  border-bottom-left-radius: 15px 13px;
}
.ossa-row[data-ossa-tail="1"] .ossa-bubble--user::after {
  right: -17px;
  width: 11px;
  background: var(--bone-white, #FBF8F2);
  border-bottom-left-radius: 9px;
}

.ossa-row[data-ossa-tail="1"] .ossa-bubble--ossa::before {
  left: -6px;
  width: 18px;
  background: var(--paper-nested, #EFE9DC);
  border-bottom-right-radius: 15px 13px;
}
.ossa-row[data-ossa-tail="1"] .ossa-bubble--ossa::after {
  left: -17px;
  width: 11px;
  background: var(--bone-white, #FBF8F2);
  border-bottom-right-radius: 9px;
}

/* ---- Typing indicator ---- */

.ossa-typing {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 11px 14px;
}
.ossa-typing i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--steel, #475569);
  opacity: 0.45;
  animation: ossa-typing 1.25s ease-in-out infinite;
}
.ossa-typing i:nth-child(2) { animation-delay: 0.16s; }
.ossa-typing i:nth-child(3) { animation-delay: 0.32s; }

@keyframes ossa-typing {
  0%, 60%, 100% { opacity: 0.32; transform: translateY(0); }
  30%           { opacity: 0.9;  transform: translateY(-3px); }
}

/* ---- The answer shape: links, copy, chips, handoff ---- */

.ossa-extra {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 6px 0 2px;
  padding-left: 2px;
}
.ossa-row--ossa + .ossa-extra { align-items: flex-start; }

.ossa-extra-label {
  font-family: var(--mono, 'JetBrains Mono', 'SF Mono', Menlo, monospace);
  font-size: 10px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--steel, #475569);
}

.ossa-chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* A quick reply is a real tappable control that sends the next turn, so it
   is a button. A source is a place to go, so it is a link. Never the two
   swapped: a link that secretly sends a message is the thing people learn
   to distrust. */
.ossa-chip {
  font: inherit;
  font-size: 13px;
  line-height: 1.2;
  padding: 7px 12px;
  border-radius: 15px;
  border: 1px solid var(--marrow-red, #2C685E);
  background: transparent;
  color: var(--marrow-red, #2C685E);
  cursor: pointer;
  text-align: left;
}
:root[data-mz-theme="dark"] .ossa-chip {
  /* The accent as TYPE needs the lifted value on a dark ground. Same hue,
     7.21 to 1 on the dark card, which is what --marrow-ember-text is for.
     Was 7.14 on the retired #5FAEB0, recomputed for #53B2A2. */
  border-color: var(--marrow-ember-text, #53B2A2);
  color: var(--marrow-ember-text, #53B2A2);
}
.ossa-chip:hover { background: var(--paper-nested, #EFE9DC); }
.ossa-chip:focus-visible { outline: 2px solid var(--marrow-red, #2C685E); outline-offset: 2px; }

.ossa-link {
  font-size: 13px;
  color: var(--marrow-red, #2C685E);
  text-decoration: underline;
  text-underline-offset: 2px;
}
:root[data-mz-theme="dark"] .ossa-link { color: var(--marrow-ember-text, #53B2A2); }

.ossa-links { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }

/* The copy control is a real button with a confirmed state, because a copy
   that silently did nothing is indistinguishable from a copy that worked. */
.ossa-copy {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font: inherit;
  font-size: 13px;
  padding: 7px 12px;
  border-radius: 10px;
  border: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  background: var(--paper-nested, #EFE9DC);
  color: var(--bone-black, #0A0908);
  cursor: pointer;
  max-width: 100%;
}
.ossa-copy:focus-visible { outline: 2px solid var(--marrow-red, #2C685E); outline-offset: 2px; }
.ossa-copy[data-ossa-copied="1"] { border-color: var(--marrow-red, #2C685E); color: var(--marrow-red, #2C685E); }
:root[data-mz-theme="dark"] .ossa-copy[data-ossa-copied="1"] {
  border-color: var(--marrow-ember-text, #53B2A2);
  color: var(--marrow-ember-text, #53B2A2);
}
.ossa-copy-value {
  font-family: var(--mono, 'JetBrains Mono', 'SF Mono', Menlo, monospace);
  font-size: 11px;
  color: var(--steel, #475569);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A handoff is an offer, so it is a button somebody can see and refuse. It
   is never a hidden link inside a sentence. */
.ossa-handoff {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  padding: 9px 14px;
  border-radius: 10px;
  border: 0;
  background: var(--marrow-red, #2C685E);
  color: var(--mzv-onfill, #FBF8F2);
  cursor: pointer;
  align-self: flex-start;
}
.ossa-handoff:focus-visible { outline: 2px solid var(--bone-black, #0A0908); outline-offset: 2px; }
.ossa-handoff[disabled] { opacity: 0.55; cursor: default; }

.ossa-steps {
  margin: 0;
  padding-left: 20px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--bone-black, #0A0908);
}
.ossa-steps li { margin: 3px 0; }

.ossa-note {
  font-size: 12px;
  line-height: 1.45;
  color: var(--steel, #475569);
}

/* ---- Composer ---- */

.ossa-composer {
  flex: 0 0 auto;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px 12px;
  border-top: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  background: var(--bone-white, #FBF8F2);
}

.ossa-input {
  flex: 1 1 auto;
  min-height: 36px;
  max-height: 120px;
  resize: none;
  padding: 8px 13px;
  border-radius: 18px;
  border: 1px solid var(--line, rgba(10, 9, 8, 0.08));
  background: var(--paper-nested, #EFE9DC);
  color: var(--bone-black, #0A0908);
  font: inherit;
  font-size: 15px;
  line-height: 1.3;
}
.ossa-input:focus-visible { outline: 2px solid var(--marrow-red, #2C685E); outline-offset: 0; }
.ossa-input::placeholder { color: var(--steel, #475569); }

.ossa-send {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: var(--marrow-red, #2C685E);
  color: var(--mzv-onfill, #FBF8F2);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.ossa-send svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.ossa-send[disabled] { opacity: 0.4; cursor: default; }
.ossa-send:focus-visible { outline: 2px solid var(--bone-black, #0A0908); outline-offset: 2px; }

/* ---- Search mode ---- */

.ossa-results { display: flex; flex-direction: column; gap: 2px; padding: 6px 0; }

.ossa-result {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 14px;
  padding: 9px 12px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--bone-black, #0A0908);
  cursor: pointer;
}
.ossa-result:hover,
.ossa-result[data-ossa-active="1"] { background: var(--paper-nested, #EFE9DC); }
.ossa-result:focus-visible { outline: 2px solid var(--marrow-red, #2C685E); outline-offset: -2px; }
.ossa-result small { display: block; color: var(--steel, #475569); font-size: 11px; margin-top: 2px; }

/* ---- Screen reader only, defined locally because a marketing page does not
        load mz-primitives.css and this widget mounts on both. ---- */
.ossa-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* =====================================================================
   3. REDUCED MOTION
   Completely static, no exception. Not a slower glow, not a shorter fade.
   The typing dots get a real static fallback rather than a frozen frame,
   because three dots stopped mid bounce reads as a broken widget.
   ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  .ossa-hive-bloom,
  .ossa-hive,
  .ossa-typing i,
  .ossa-panel,
  .ossa-intro,
  [data-ossa-open] svg,
  svg[data-ossa-open] {
    animation: none !important;
    transform: none !important;
    transition: none !important;
  }
  /* The still mark: disc, lit core, bone comb and a halo settled at 0.7.
     Nothing about it depends on motion to read. */
  .ossa-hive-bloom { opacity: 0.7; }
  .ossa-hive-lattice polygon { transition: none; }
  .ossa-typing i { opacity: 0.45; }
  .ossa-launcher-word { transition: none; }
  .ossa-launcher--pill { transition: none; }
  /* The introduction simply is there, then is not. Its caret keeps its
     turn, because that is a shape and not a motion. */
  .ossa-intro[data-ossa-leaving="1"] { opacity: 0; }
  [data-ossa-open] use[fill] { transition: none; }
}
