/* AI Ellie chat widget — MyPashmina.

   Refinement round 260729 (Paul's "final premium" direction, with two
   reference renders — light and dark). The plum/sage accent scheme is
   RETIRED: the widget now runs on warm neutrals alone, with black (light
   mode) / cream (dark mode) as the single primary. The design must read
   as a luxury retailer or hotel concierge, never as a chatbot, and the
   two themes must read as one product seen twice — not two designs.

   Values below are sampled from Paul's renders, not invented.

   The five rules that keep it there:
     · separation comes from SPACING, SHADOW and ALIGNMENT — never from
       an outline you can see or a coloured box;
     · shadows are paper-resting-on-paper, never Material elevation;
     · typography is the design; corners stay generous;
     · exactly one accent family (warm sand) for "selected"/"done";
     · nothing purple, blue or neon, in either theme. */

:root {
  /* warm ivory — NOT pure white; white is reserved for the customer's
     own bubbles so they read as sitting on top of the page */
  --aec-ivory: #faf8f4;
  --aec-ivory-2: #f1ede5;
  /* customer name pill — a warm neutral grey that clears BOTH the white
     bubble and the ivory window (Paul 260722; the old blue nudge went
     with the "no blue" ruling 260729) */
  --aec-tag-customer: #dedbd5;
  /* bubbles are FLAT now (260729): the old top-to-bottom gradients read
     as a device, and depth is the shadow's job. The -2 tokens are kept
     equal to their tops so any remaining gradient is a no-op. */
  --aec-surface-2: #ffffff;
  --aec-bubble-agent-2: #f1e9de;
  /* Ellie's bubble: warm beige, sitting CLEARLY below the ivory page.
     It used to be #f6f2ea — "a breath below", which on a real screen was
     no separation at all (Paul 260802: "the bubbles do not contrast
     enough from the background"). #f1e9de is his hex; it takes the
     bubble/page ratio from 1.05 to 1.13, roughly tripling the step,
     while staying inside the warm-neutral palette. The customer's bubble
     stays pure white — it is already at the ceiling, so its separation
     comes from the (now stronger) shadow below. */
  --aec-bubble-agent: #efe6d6;
  /* Ellie's name pill, darkened with her bubble so it still reads as a
     pill sitting on top of it (1.17 against the bubble, 1.33 the page) */
  --aec-bubble-agent-line: #e3d8c5;
  /* soft warm ring for calm "listening"/success pulses */
  --aec-sage-ring: rgba(160, 140, 112, .28);
  --aec-surface: #ffffff;
  /* the customer's message bubble only — see .aec-msg-bubble */
  --aec-bubble-customer: #ffffff;
  --aec-bubble-customer-line: #e5ded1;
  --aec-charcoal: #1a1917;
  --aec-charcoal-soft: #5d574e;
  /* --aec-ink replaces the retired plum: quiet near-black emphasis for
     links, typing dots and spinners. Colour is NOT the affordance any
     more — links carry weight + underline, so the ink can be the same
     black as body copy (§1 "black text", §2 "no coloured boxes"). */
  --aec-ink: #1a1917;
  --aec-focus: #1a1917;
  /* the single accent family: warm sand. Used for the selected chip,
     the done-tick disc and quiet fills — never for text emphasis. */
  --aec-accent: #f2eae0;
  --aec-accent-line: #e6dbcb;
  --aec-chip-on-bg: #f0e7da;
  --aec-chip-on-fg: #1a1917;
  /* "sage" survives only as the human-colleague identity and the done
     tick — desaturated so it belongs to a warm neutral palette */
  --aec-sage: #8d9a80;
  --aec-sage-text: #5c6b4f;
  --aec-sage-bg: #eef0e8;
  /* the colleague's name pill, darkened off his bubble by the same step
     Ellie's is off hers (~1.16) — since the bubble lost its outline
     (Paul 260818), the pill's own shade is what says "a pill sitting on
     top of it" */
  --aec-sage-tag: #dee0d8;
  --aec-line: #e7e2d9;
  --aec-danger: #9c4a3c;
  /* the same red with a third of the saturation, for a destructive
     row that is resting rather than warning */
  --aec-danger-quiet: #8a5f57;
  --aec-danger-bg: #9c4a3c;
  /* hover DARKENS. The old rule brightened, which on a filled danger
     button walks the ground towards its own white text (Paul 260811 #1). */
  --aec-danger-hover: #7d3628;
  /* ONE primary: black in light, cream in dark. -fg travels with it so
     every primary control flips text colour with the theme. */
  --aec-primary-bg: #141312;
  --aec-primary-hover: #33302c;
  --aec-primary-fg: #ffffff;
  --aec-banner-bg: #f3ece0;
  --aec-banner-text: #1a1917;
  /* nudged down with Ellie's darker bubble — 4.51:1 there was AA by a
     hair, #6b655c restores headroom (4.79 on her bubble, 5.77 on white) */
  --aec-time: #6b655c;
  --aec-radius: 16px;
  --aec-gutter: 16px;               /* ONE horizontal grid for all rows */
  /* 70px while the bar carried a two-line tagline; 56px now that it
     carries two controls and nothing else (Paul 260813). Every header
     offset in this file derives from it, so the whole screen moves up
     with it — which is where the reclaimed room goes. */
  --aec-head-h: 56px;
  --aec-glass: rgba(250, 248, 244, .84);
  /* header only: airier glass so scrolling text ghosts through
     (260721h #3) — and one step LESS airy once scrolled, so the
     controls stay crisp over moving text (260813) */
  --aec-glass-head: rgba(250, 248, 244, .68);
  --aec-glass-head-scrolled: rgba(250, 248, 244, .88);
  /* the header's own hairline: a shade stronger than the general glass
     line, because it fades to nothing across the empty half and needs
     to still register where it does land */
  --aec-head-line: rgba(26, 25, 23, .11);
  /* the intro's primary launcher: ~10% darker than --aec-surface in
     HSL lightness (100% → 89%), same warm hue as the rest */
  --aec-welcome-primary: #ede7de;
  --aec-welcome-primary-hover: #e5ded2;
  --aec-welcome-primary-line: #ded5c7;
  /* placeholder text: a step darker than the general soft grey, which
     on the composer's pale ground was close to reading as disabled */
  --aec-placeholder: #4f4a42;
  /* the wash behind an open menu — a suggestion of depth, never a
     modal veil */
  --aec-scrim: rgba(26, 25, 23, .04);
  /* the greeting's robot: 30 points of HSL lightness QUIETER than the
     word beside it (--aec-ink, L 9.6% → 39.6%). It is the mark for who
     is speaking, not a second headline. */
  --aec-hello-bot: #6b665f;
  /* the raised cell inside a segmented pill (chosen Appearance, and
     the A− / A+ ends of Text size) */
  --aec-seg-raised: #ffffff;
  /* light: Send IS the primary — black on cream, nothing to soften */
  --aec-send-bg: var(--aec-primary-bg);
  --aec-send-hover: var(--aec-primary-hover);
  --aec-send-fg: var(--aec-primary-fg);
  /* waiting, not refusing: the disc recedes to the composer's own
     surface and the arrow to the quiet ink — still legible, no longer
     the loudest thing on an empty screen */
  --aec-send-idle-bg: #efece6;
  --aec-send-idle-fg: #8b857b;
  /* input box: its own tint — dark must run MUCH airier or the
     ghosting is invisible on real phones (260722q).
     A LIGHTER shade than the panel around it (Paul, 260820d). Light
     mode has almost no headroom left — the panel is already ivory at
     96.9% lightness — so the step is taken to pure white and held a
     little more opaque, which is what actually separates the typing
     area from the wash behind it. */
  --aec-glass-input: rgba(255, 255, 255, .78);
  /* The line above the typing area, 50% lighter (Paul, 260820d:
     "similar colour to right hand end of top divider line"). Those two
     halves point in slightly different directions — the header line's
     strong end is `--aec-head-line`, which blends to about the same
     value as the solid `--aec-line` rather than a lighter one — so
     this takes the instruction literally: `--aec-line` (#e7e2d9)
     halfway to the ivory behind it, in the same warm family as the
     header's. */
  --aec-input-line: #f0ede6;
  --aec-glass-strong: rgba(250, 248, 244, .94);
  --aec-glass-line: rgba(26, 25, 23, .07);
  /* paper resting on paper (§12) — never Material elevation */
  --aec-shadow: 0 8px 30px rgba(26, 25, 23, .1),
                0 1px 3px rgba(26, 25, 23, .05);
  /* the white customer bubble on ivory is a 1.06 step — the tint cannot
     carry it, so the shadow does (Paul 260802). Still paper-on-paper. */
  --aec-shadow-bubble: 0 1px 2px rgba(26, 25, 23, .07),
                       0 4px 14px rgba(26, 25, 23, .06);
  --aec-shadow-card: 0 1px 2px rgba(26, 25, 23, .04),
                     0 6px 20px rgba(26, 25, 23, .05);
  --aec-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
      "Helvetica Neue", Arial, sans-serif;
  --aec-serif: "Playfair Display", "Cormorant Garamond", Didot,
      Georgia, "Times New Roman", serif;
  /* the MyPashmina logo is Century Gothic — headers and name labels
     match it (Paul 260721 #7). Questrial (webfont, enqueued by the
     plugin) and Futura cover devices without Century Gothic. */
  --aec-display: "Century Gothic", CenturyGothic, Questrial,
      "Futura PT", Futura, "Trebuchet MS", sans-serif;
}

/* ── Night mode ─────────────────────────────────────────────────────
   data-aec-theme on #aiellie-root is only ever "light" or "dark" — the
   script resolves the menu's "Auto" against the visitor's clock before
   it touches the DOM (Paul 260803: night mode was showing at noon
   because the OS said dark). One variable set restyles everything, and
   the byte-identical prefers-color-scheme copy that used to live below
   is gone with it — there is nothing left to keep in sync. */
#aiellie-root[data-aec-theme="dark"] {
  /* Dark: a luxury hotel lobby at night. Warm charcoal throughout —
     never blue-black, never purple. Ellie's bubble sits LIGHTER than
     the page so her text reads; the customer's sits between the two.
     The primary flips to cream: a black button on a near-black page is
     not a button.
     260802: both bubbles came up. The customer's was #1e1b18 on a
     #141210 page — 1.09:1, which is not a bubble, it is a rumour. The
     ladder is now page 1.00 → customer 1.31 → Ellie 1.61, so each is
     plainly its own surface and they are still plainly different from
     each other (1.23 between them). */
  --aec-ivory: #141210;
  --aec-ivory-2: #211d18;
  --aec-tag-customer: #403a33;
  --aec-bubble-agent: #3e3830;
  --aec-bubble-agent-2: #3e3830;
  --aec-bubble-agent-line: #524b40;
  --aec-surface: #1e1b18;
  --aec-surface-2: #1e1b18;
  --aec-bubble-customer: #2e2a24;
  --aec-bubble-customer-line: #474036;
  --aec-sage-ring: rgba(190, 176, 150, .3);
  --aec-charcoal: #f1ebe2;
  /* +1 step on the three quiet things: secondary text, hairlines and
     button outlines were all correct in isolation and collectively a
     little under-said (Paul 260813). Nothing here moves more than a
     few points — dark mode was already close. */
  --aec-charcoal-soft: #b6ae9f;
  --aec-ink: #f1ebe2;
  --aec-focus: #c9bda9;
  --aec-accent: #2b2620;
  --aec-accent-line: #3b342b;
  --aec-chip-on-bg: #c8b6a8;
  --aec-chip-on-fg: #17150f;
  --aec-sage: #8d9a80;
  --aec-sage-text: #a8b79a;
  /* lifted with the other two bubbles — the human colleague's must not
     be the one surface left sitting in the page (mic-on shares it) */
  --aec-sage-bg: #333a2c;
  /* lightened off the bubble in dark, same step as Ellie's pill */
  --aec-sage-tag: #474d3c;
  --aec-line: #3d3831;
  --aec-danger: #d38a79;
  --aec-danger-quiet: #bb9186;
  --aec-danger-bg: #7f3d31;
  --aec-danger-hover: #96493a;
  --aec-primary-bg: #decdc0;
  --aec-primary-hover: #ecdccf;
  --aec-primary-fg: #17150f;
  --aec-banner-bg: #322a20;
  --aec-banner-text: #f0e5d2;
  /* tuned against Ellie's bubble, the LIGHTEST surface a timestamp sits
     on in dark mode. She moved to #3e3830, which took the old #a49c8d
     to 4.26:1 — under AA; #b0a898 restores it (4.91 on her bubble,
     6.04 on the customer's) */
  --aec-time: #b0a898;
  --aec-shadow: 0 10px 34px rgba(0, 0, 0, .5),
                0 1px 3px rgba(0, 0, 0, .4);
  --aec-shadow-bubble: 0 1px 2px rgba(0, 0, 0, .3),
                       0 4px 14px rgba(0, 0, 0, .22);
  --aec-shadow-card: 0 1px 2px rgba(0, 0, 0, .3),
                     0 6px 20px rgba(0, 0, 0, .26);
  --aec-glass: rgba(20, 18, 16, .82);
  --aec-glass-head: rgba(20, 18, 16, .64);
  --aec-glass-head-scrolled: rgba(20, 18, 16, .86);
  /* the same 10% step, in the direction dark mode has room for:
     rgb(24,21,18) is 8.2% lightness, so 18.2% lands here (260820d) */
  --aec-glass-input: rgba(53, 46, 40, .5);
  /* dark keeps the panel's own divider: the border is already a light
     line on a dark ground, so "lighter" would erase it. */
  --aec-input-line: var(--aec-line);
  --aec-glass-strong: rgba(20, 18, 16, .94);
  --aec-glass-line: rgba(241, 235, 226, .13);
  --aec-head-line: rgba(241, 235, 226, .16);
  /* the same 10% step, in the other direction: LIGHTER than the
     surface the secondaries are filled with (10.6% → 20.6%) */
  --aec-welcome-primary: #3b342e;
  --aec-welcome-primary-hover: #473f37;
  --aec-welcome-primary-line: #4a4239;
  --aec-placeholder: #b6ae9f;
  --aec-scrim: rgba(0, 0, 0, .16);
  /* the same 30 points of quiet, in the other direction: DARKER than
     the word it stands beside (--aec-ink, L 91.6% → 61.6%) */
  --aec-hello-bot: #a99f91;
  /* dark's --aec-surface is within a point of the pill's own ground,
     so a raised cell filled with it would be invisible — it needs its
     own step up */
  --aec-seg-raised: #37312b;
  /* Send is the only PLACE the warm cream primary sits on its own,
     surrounded by neutral dark — which is what made it read as peach
     rather than as "the primary, in dark" (Paul 260813). It keeps the
     primary's job and loses the warmth: a near-neutral bone, same
     lightness, a third of the saturation. Everything else the primary
     fills (switches, hovers) sits ON something and is left alone. */
  --aec-send-bg: #ded9d2;
  --aec-send-hover: #ece7e0;
  --aec-send-fg: #17150f;
  --aec-send-idle-bg: #2b2722;
  --aec-send-idle-fg: #7d766c;
}

/* page lock while the mobile chat is open — the underlying WordPress
   page neither scrolls nor peeks through */
html.aec-locked, html.aec-locked body {
  overflow: hidden !important;
  height: 100%;
}
html.aec-locked body { position: fixed; width: 100%; }

#aiellie-root, #aiellie-root * { box-sizing: border-box; }
/* display:flex on the widget containers would otherwise defeat the
   hidden attribute — a hidden window must never intercept clicks. */
#aiellie-root [hidden] { display: none !important; }
#aiellie-root {
  font-family: var(--aec-font);
  /* --aec-font-scale: customer-set via the menu's Font size − / + */
  font-size: calc(16.5px * var(--aec-font-scale, 1));
  line-height: 1.5;
  color: var(--aec-charcoal);
  font-weight: 400;
}
#aiellie-root button { font-family: inherit; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
/* every button eases to 96% while pressed (260721h #6). :where()
   keeps specificity at zero so component transition lists still win —
   any rule declaring its own `transition` must include transform. */
:where(#aiellie-root) button { transition: transform .09s ease; }
:where(#aiellie-root) button:active:not([disabled]) {
  transform: scale(.96);
}
#aiellie-root button:focus-visible,
#aiellie-root a:focus-visible,
#aiellie-root textarea:focus-visible,
#aiellie-root input:focus-visible {
  outline: 2px solid var(--aec-focus);
  outline-offset: 2px;
}
@media (max-width: 640px) {
  #aiellie-root { font-size: calc(17px * var(--aec-font-scale, 1));
    line-height: 1.5; }
}

/* ── Launcher ─────────────────────────────────────────────────────── */
/* #aiellie-root prefix everywhere: theme button styles were washing the
   launcher out to white-on-light-grey on the shop (Paul 260721 #8) */
/* 260729: the launcher is the first thing anyone sees of the chat, so it
   is held to the same rules as the panel — one solid primary, no plum
   gradient, no ✦ sparkles (§4: the decorative AI/star mark goes and is
   NOT replaced), no sheen/glow/wiggle loop (§14: nothing flashy). What
   is left is a quiet pill in the shop's own ink. */
/* Paul 260809d: "move launcher on desktop to a better position and also
   put the hide button in. so it is more similar to mobile version".

   Both rigs now work the same way. The launcher stands on the centre
   line of the panel it becomes, at the height the composer's ▾ Hide
   chat pill will occupy — so on desktop, as on a phone, the chat goes
   back into the button your pointer is already on. Derived from the
   panel's own geometry (--aec-dock-*), never from a number typed twice:
   the band below is deliberately asymmetric so that `margin: auto`
   centres the pill on the PANEL's centre rather than the viewport's.

   It also bought the desktop its enlarged-icon moment. The old corner
   position had the launcher inside the window's own bottom-right corner
   with half its height beneath it, so the handover rectangle could not
   grow at all without being cropped by the window edge, and desktop
   fell back to a plain swap at natural size. On the centre line there
   is room, and desktop now animates exactly as the phone does. */
:root, #aiellie-root {
  --aec-dock-right: 22px;    /* the panel's own offset */
  --aec-dock-w: 420px;       /* the panel's own width */
  /* the centre line of the launcher, above the bottom of the SCREEN:
     the panel's 22px offset plus the 36px the Hide pill sits at */
  --aec-anchor-centre: 58px;
}
#aiellie-root .aec-launcher {
  position: fixed; z-index: 99990;
  left: 0;
  right: calc(2 * (var(--aec-dock-right) + var(--aec-dock-w) / 2) - 100vw);
  margin-left: auto; margin-right: auto;
  width: -moz-fit-content; width: fit-content;
  bottom: calc(var(--aec-anchor-centre) - 24px
               + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 9px;
  padding: 14px 22px;
  /* the pill's own fill is on ::before, not here — see the living edge
     below: the light has to get UNDER the button, and a child cannot
     paint beneath its parent's own background */
  background: transparent;
  color: var(--aec-primary-fg);
  border: 0;
  border-radius: 28px;
  box-shadow: var(--aec-shadow);
  font-family: var(--aec-display);
  font-size: 16px; font-weight: 600; letter-spacing: .01em;
  transition: transform .09s ease;
  /* the fourth number tuned alongside the living edge's colours, live,
     against the same rendered picture — kept as a filter rather than
     baked into the gradient stops because a browser's saturate-filter
     math (a matrix operation on rendered RGB) is not the same
     arithmetic as scaling each colour's own HSL saturation by hand;
     baking it would not reproduce what was actually approved */
  filter: saturate(300%);
}
/* the green "we're online" dot went with the header's status indicator
   (§3) — a launcher that is there IS the availability signal */
.aec-launcher .aec-badge { margin-left: 2px; }

/* ── The living edge (Paul, 260823, redone 260824) ─────────────────
   Three looks in a day, all three ruled on by him:

   > it looks bad. the colour glow reveals a straight edge. it doesnt
   > move smoothly. too faded. too low quality
   >
   > try this [the Gemini rotating conic-gradient border]

   — which gave the RING below: a conic gradient on a square behind the
   button, clipped to the pill, with the button's own surface sitting
   inside it. A vector, so it is exact at any size and any pixel
   density, never blurred, never faded, never a photograph.

   Then, tuned by hand in a standalone playground and handed back as a
   settings block ("implement with these settings"), the GLOW arrived:
   three independent balls behind the ring, each drifting/breathing/
   deforming on its own — see livingEdge() in aiellie.js for what that
   needed to change (a `requestAnimationFrame` loop in place of the
   ring's old `@keyframes` rotation, so an UNEVEN turning speed and
   balls that wander are even expressible). The ring's own gap-proof
   geometry is untouched by any of it — see the JS.

   BOTH controls carry it, and the minimised tab is the important one:
   nothing in the widget ever puts windowMode back to 'closed', so the
   launcher is what a FIRST-TIME visitor sees and the ▾ tab is what
   everybody else sees, forever.

   Layers, in this order, inside the control's own stacking context:

     z -1  .aec-glow  three drifting balls — NOT clipped, free to peek
                      past the pill on any side
     z  0  .aec-edge  the clipped, rotating gradient ring — inert
     z  1  ::before   the surface, inset by the ring's width
     z  2  the label  'Ask AI Ellie' / '💬 Chat', and its badge

   Both are CHILDREN of the control on purpose. Every transform the
   launcher already owns — the visual-viewport lift, the press-scale,
   the morph's per-axis scale — carries them without a second writer
   for their position, which is the mistake the 260809 round paid for
   once already. */
:root, #aiellie-root {
  --aec-ring: 1px;
  /* one palette, read by the ring AND every glow ball — tuned in the
     playground as hue/sat/bright per colour; kept here as literal hex
     for the same reason the first round avoided color-mix(): baseline
     compatibility over syntax convenience. First colour repeated at
     the end so the sweep closes without a seam. */
  --aec-gradient: conic-gradient(#5c83c1, #8658bb, #f3dddf, #d0a93e, #5c83c1);
}

/* The clip. overflow:hidden plus the pill's own radius is what turns a
   spinning square into a ring; border-radius: inherit so it can never
   disagree with the button it is cut from. */
#aiellie-root .aec-launcher .aec-edge,
#aiellie-root .aec-mintab .aec-edge {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  overflow: hidden;
  /* it must never eat the tap it is advertising */
  pointer-events: none;
  animation: aec-edge-in .5s ease both;
}
/* The gradient itself. Sized and centred by the JS — a square of the
   button's own diagonal, which is the smallest one that still covers
   the button at every angle. Its rotation is written by JS every
   frame now (`requestAnimationFrame`, not `@keyframes`) — the only way
   to give it an UNEVEN turning speed — but nothing here changed to
   allow that: the browser doesn't care who sets `transform`, only that
   nothing else about the element (its shape, above all) also moves. */
#aiellie-root .aec-edge-spin {
  position: absolute;
  background: var(--aec-gradient);
  will-change: transform;
}
@keyframes aec-edge-in { from { opacity: 0; } to { opacity: 1; } }
/* The glow: three balls, NOT clipped — the halo is meant to escape the
   pill on whichever side a ball has drifted toward. Shape, size and
   position are written by JS every frame (see livingEdge in
   aiellie.js); this only declares what CSS is allowed to touch. */
#aiellie-root .aec-launcher .aec-glow,
#aiellie-root .aec-mintab .aec-glow {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: visible;
  pointer-events: none;
  animation: aec-edge-in .5s ease both;
}
#aiellie-root .aec-glow-ball {
  position: absolute;
  background: var(--aec-gradient);
  will-change: transform;
}
/* Dark while the chat is in flight. The morph scales whichever control
   the chat is coming out of by the panel's own ratio — on a phone that
   is ~8x — and there is nothing to see under a button already fading to
   nothing. It comes back when the flight settles, with the fade-in. */
#aiellie-root .aec-launcher.aec-morphing-anchor .aec-edge,
#aiellie-root .aec-launcher.aec-morphing-anchor .aec-glow,
#aiellie-root .aec-mintab.aec-morphing-anchor .aec-edge,
#aiellie-root .aec-mintab.aec-morphing-anchor .aec-glow { display: none; }
@media (prefers-reduced-motion: reduce) {
  /* Asked for less motion: the colours stay, nothing turns or drifts.
     livingEdge()'s own sync() is what actually stops the JS loop; this
     is only the CSS-side guard for the mount fade. */
  #aiellie-root .aec-edge, #aiellie-root .aec-glow { animation: none; }
}

/* The surface, inset by the ring so the ring is what shows around it.
   This is the fill that used to be `background` on the button itself —
   a child cannot paint beneath its parent's own background, and the
   light has to come from behind. */
#aiellie-root .aec-launcher::before {
  content: '';
  position: absolute;
  inset: var(--aec-ring);
  z-index: 1;
  border-radius: inherit;
  background: var(--aec-primary-bg);
  transition: background-color .16s ease;
}
#aiellie-root .aec-launcher:hover::before {
  background: var(--aec-primary-hover);
}
/* The tab is frosted glass rather than solid ink, so its ::before takes
   the backdrop-filter with it. Its own hairline border goes transparent
   rather than away: it is part of the layout box, and removing it would
   have taken 2px off the tab and shifted its text. */
#aiellie-root .aec-mintab::before {
  content: '';
  position: absolute;
  inset: var(--aec-ring);
  z-index: 1;
  border-radius: inherit;
  background: var(--aec-glass-strong);
  -webkit-backdrop-filter: blur(14px) saturate(1.5);
  backdrop-filter: blur(14px) saturate(1.5);
}
#aiellie-root .aec-launcher > span,
#aiellie-root .aec-mintab > span { position: relative; z-index: 2; }

/* mobile: the launcher sits ON the Hide-chat pill's spot (260809b).
   It used to sit 102px up — Paul 260720, because a bottom-22px launcher
   fell below the visible fold on pages with odd layout viewports. That
   ruling is superseded by his 260809 one ("it should be right at the
   bottom of the screen ... in exactly the same place as the hide chat
   button"), and the fold problem keeps its OTHER guard from that same
   round: positionFloaters() measures the overhang and lifts by exactly
   it, which the static 80px was only ever an approximation of.

   36px above the bottom is not a taste: it is where the composer puts
   ▾ Hide chat (an 18px gap under a 36px pill), and the two controls are
   the same journey in opposite directions. The offsets below are that
   centre line minus each control's own half-height, so a customer who
   hides the chat watches it shrink into the button their thumb is
   already on. --aec-anchor-centre keeps the two in step; the E2E
   asserts the centres land within 2px of each other. */
@media (max-width: 640px) {
  /* #aiellie-root prefix: must outrank the base launcher rule, which
     carries the same prefix to beat theme button styles */
  #aiellie-root .aec-launcher, #aiellie-root .aec-mintab {
    /* full screen, so the panel's bottom IS the screen's bottom and the
       Hide pill's 36px is measured straight off it */
    --aec-anchor-centre: 36px;
    right: 0;
    /* BOTTOM CENTRE on a phone (260809). The chat now grows out of the
       launcher and shrinks back into it, and a full-screen panel can
       only plausibly come from — and return to — a point on its own
       centre line. A right-corner anchor made the expansion read as a
       diagonal wipe; centred, it reads as the button opening.
       Centred with auto margins, NOT translateX(-50%): the transform
       channel belongs to positionFloaters() (visual-viewport lift) and
       to the press-scale, and a second writer silently wins. */
    left: 0; right: 0;
    margin-left: auto; margin-right: auto;
    width: -moz-fit-content; width: fit-content;
    max-width: calc(100vw - 32px);
  }
  /* 48px tall (14px padding + 20px line) */
  #aiellie-root .aec-launcher {
    bottom: calc(var(--aec-anchor-centre) - 24px
                 + env(safe-area-inset-bottom));
  }
  /* 46px tall (12px padding + 22px line) */
  #aiellie-root .aec-mintab {
    bottom: calc(var(--aec-anchor-centre) - 23px
                 + env(safe-area-inset-bottom));
  }
}

/* ── Minimised tab ────────────────────────────────────────────────── */
.aec-mintab {
  position: fixed; z-index: 99990;
  left: 0;
  right: calc(2 * (var(--aec-dock-right) + var(--aec-dock-w) / 2) - 100vw);
  margin-left: auto; margin-right: auto;
  width: -moz-fit-content; width: fit-content;
  bottom: calc(var(--aec-anchor-centre) - 23px
               + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 10px;
  /* 13px, not 12px + a 1px border. The hairline border is gone — the
     rainbow ring is the tab's edge now — and its width moved into the
     padding so the box is the same 46px it has always been (13 + 20 +
     13), which the morph's anchor arithmetic depends on.

     It had to go rather than merely turn transparent: `inset: 0` on an
     absolutely positioned child resolves against the PADDING box, so a
     1px border held the ring 1px inside the tab and left its outermost
     pixel showing the shop's page. The launcher, which has no border,
     was fine — which is exactly why the ring check asks both. */
  padding: 13px 19px;
  filter: saturate(300%);   /* same fourth number, see .aec-launcher */
  /* glass and blur are on ::before, above the living edge — see it */
  background: transparent;
  color: var(--aec-charcoal);
  border: 0; border-radius: 26px;
  box-shadow: var(--aec-shadow);
  font-family: var(--aec-display); font-size: 15px; font-weight: 600;
}
.aec-mintab .aec-spinner { width: 15px; height: 15px; }
.aec-mintab-status {
  color: var(--aec-charcoal-soft); font-size: 14px; font-weight: 400;
  max-width: 190px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}

.aec-badge {
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: 10px;
  background: var(--aec-primary-bg); color: var(--aec-primary-fg);
  font-size: 13px; font-weight: 600;
  display: inline-flex; align-items: center; justify-content: center;
}

/* ── Window ───────────────────────────────────────────────────────── */
.aec-window {
  position: fixed; right: 22px; bottom: 22px; z-index: 99991;
  width: 420px; height: 95vh;
  max-height: calc(100vh - 44px); max-width: calc(100vw - 44px);
  min-width: 330px; min-height: 430px;
  background: var(--aec-ivory);
  border: 1px solid var(--aec-line); border-radius: var(--aec-radius);
  box-shadow: var(--aec-shadow);
  display: flex; flex-direction: column;
  overflow: hidden;
  resize: none;   /* the top-left grip does the resizing */
}
.aec-window.aec-max {
  width: min(780px, calc(100vw - 44px));
  height: calc(100vh - 44px);
  resize: none;
}
.aec-window.aec-docked {
  top: 0; right: 0; bottom: 0; left: auto;
  height: 100vh; max-height: none; max-width: none; min-width: 340px;
  border-width: 0 0 0 1px; border-radius: 0;
  box-shadow: -2px 0 10px rgba(38, 36, 31, .06);
}
html.aec-dock-active body {
  width: calc(100% - var(--aec-dock-width));
  max-width: calc(100% - var(--aec-dock-width));
  overflow-x: clip;
  transition: width .18s ease, max-width .18s ease;
}
html.aec-resizing, html.aec-resizing * {
  cursor: col-resize !important;
  user-select: none !important;
}
html.aec-resizing body { transition: none; }
html.aec-dock-preferred:not(.aec-dock-active) #aiellie-root .aec-launcher,
html.aec-dock-preferred:not(.aec-dock-active) #aiellie-root .aec-mintab {
  /* The closed control occupies the same horizontal band as the open
     dock. Auto margins therefore put its centre on the composer's
     centred Hide-chat button, regardless of the saved dock width. */
  left: calc(100vw - var(--aec-dock-width));
  right: 0;
  margin-left: auto; margin-right: auto;
}
@media (max-width: 640px) {
  .aec-window, .aec-window.aec-max {
    position: fixed; inset: 0; right: 0; bottom: 0; top: 0; left: 0;
    width: 100vw; height: 100%;
    height: 100dvh; max-width: 100vw; max-height: 100dvh;
    /* the desktop floor is not a floor here: full screen IS the size,
       and 330px of it on a 320px phone hung 10px off the right edge —
       enough to put the chat's own centre line 5px off the launcher's
       (found at 320px, 260809) */
    min-width: 0; min-height: 0;
    border-radius: 0; border: 0; resize: none;
    z-index: 2147483000;   /* above any theme/shortcode stacking */
  }
  .aec-head { padding-top: env(safe-area-inset-top);
    height: calc(var(--aec-head-h) + env(safe-area-inset-top)); }
}

/* ── Launcher ⇄ window morph (260809) ──────────────────────────────
   The chat must be SEEN to come out of the launcher and go back into
   it. On a phone the full-screen panel otherwise arrives like a page
   navigation — the customer's own words for it are "where has the shop
   gone?" — and the answer is not to shrink the chat, it is to show the
   journey. The geometry is computed in the JS (morphWindow) from the
   launcher's real bounding rect, so there is no hard-coded origin here
   and the anchor stays true at any width, orientation, safe-area inset
   or scroll position. This block only declares what the transition is
   allowed to touch. */
.aec-window.aec-morphing {
  /* the panel is mid-flight: nothing in it is a real target yet, and a
     tap landing on a half-grown control is a tap the customer did not
     mean to make */
  pointer-events: none;
  /* NO will-change (260809c). It was here on the usual reasoning — tell
     the browser what is about to move — and measurement says it makes
     this animation worse, not better: promoting a full-screen panel to
     its own layer AND hinting clip-path pushes Chrome onto a path that
     re-rasters the whole layer per frame. Interleaved A/B at 6x CPU
     throttle, 12 flights each: worst frame per flight median 84ms →
     75ms and p90 186ms → 130ms with the hint REMOVED. A hint that
     costs memory and buys nothing does not stay. */
}
/* The panel, rendered once while nobody is looking (260809c).

   A chat that has never been opened has never been laid out or painted
   either, and that bill falls due in the frame the customer taps — on a
   mid-range phone, measured at 6x CPU throttle, it held the panel frozen
   at launcher size for 214–448ms before the transition could even
   start, and in the worst trial the whole 300ms flight played four
   frames. Doing it once at idle moves the cost to a moment nobody is
   watching. Invisible and untouchable while it happens; a class rather
   than inline styles so it cannot collide with the morph's own opacity
   if the customer taps mid-warm. */
.aec-window.aec-warming {
  opacity: 0 !important;
  pointer-events: none !important;
}
/* the launcher cross-fades with the panel it becomes — set by the JS,
   which owns the timing so open and close stay mirror images */
#aiellie-root .aec-launcher.aec-morphing-anchor,
#aiellie-root .aec-mintab.aec-morphing-anchor {
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  /* asked for less motion: the chat simply IS open, or IS closed.
     No scale, no travel — the JS skips the whole morph, and this is
     the guard for anything that reaches the element another way. */
  .aec-window.aec-morphing {
    transition: none !important; transform: none !important;
    clip-path: none !important; opacity: 1 !important;
  }
  #aiellie-root .aec-launcher.aec-morphing-anchor,
  #aiellie-root .aec-mintab.aec-morphing-anchor {
    transition: none !important;
  }
}

/* drag-and-drop target (Paul 260804) — a drop with no visible target
   is a guess, and a guess that fails opens the file in the browser and
   loses the page. An ::after overlay rather than a border so nothing in
   the conversation reflows while the pointer is over it, and
   pointer-events:none so it can never eat the drop it is advertising. */
.aec-window.aec-dropping::after {
  content: 'Drop your photo here';
  position: absolute; inset: 12px; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  border: 2px dashed var(--aec-primary-bg);
  border-radius: calc(var(--aec-radius) - 4px);
  background: var(--aec-glass-head);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
  backdrop-filter: blur(10px) saturate(1.4);
  color: var(--aec-ink); font-size: 16px; font-weight: 500;
  pointer-events: none;
}

/* header — floats over the conversation (functional glass): messages
   scroll beneath it into the fade.

   260813: the bar holds Human help and Menu, both on the right, and
   nothing at all on the left. Two consequences are deliberate:

     * the rule beneath it FADES OUT towards the empty side. A hard
       full-width line under an empty left half draws a border around
       nothing; a line that arrives with the controls reads as their
       underscore. (border-bottom cannot be a gradient, so the line is
       an ::after — it also lets the scrolled state raise only the
       line's opacity without touching the bar's own geometry.)
     * at rest the glass is airy enough for text to ghost through; on
       scroll it thickens (see .aec-head-shadow below) so passing
       messages stay faint while the controls stay crisp. */
.aec-head {
  position: absolute; top: 0; left: 0; right: 0; z-index: 5;
  height: var(--aec-head-h);
  display: flex; align-items: center; gap: 6px;
  /* buttons sit ~10px further right on desktop (Paul 260723 #6);
     the mobile media block restores the full gutter */
  padding: 0 6px 0 var(--aec-gutter);
  background: var(--aec-glass-head);
  -webkit-backdrop-filter: blur(12px) saturate(1.5);
  backdrop-filter: blur(12px) saturate(1.5);
  transition: background-color .25s ease,
              -webkit-backdrop-filter .25s ease,
              backdrop-filter .25s ease;
}
.aec-head::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 1px; pointer-events: none;
  /* transparent at the empty left, full strength under the controls */
  background: linear-gradient(to right,
              transparent 0%, var(--aec-head-line) 46%,
              var(--aec-head-line) 100%);
  opacity: .7; transition: opacity .25s ease;
}
/* the empty left half — a flex spacer, so Human help and Menu are
   pushed to the right without either of them having to know it */
.aec-head-space { flex: 1 1 auto; min-width: 0; }
/* (collapsing header removed — Paul 260722r: it stays as it is) */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .aec-head { background: var(--aec-glass-strong); }
  .aec-window.aec-dropping::after { background: var(--aec-glass-strong); }
}
/* (.aec-head-id / .aec-title / .aec-tagline are GONE — Paul 260813.
   The name and the tagline moved into the greeting bubble; the rules
   went with the markup rather than being left behind as dead weight
   that a later round would have to guess about.) */

/* SCROLLED (§3): content passing behind must stay faint while the
   controls stay crisp — so the glass thickens and the blur deepens,
   and the fading rule comes up a little to say the bar is now sitting
   ON something. Explicitly NOT a drop shadow (Paul 260813): a shadow
   lifts the bar off the page and the widget is paper on paper (§12). */
.aec-window.aec-head-shadow .aec-head {
  background: var(--aec-glass-head-scrolled);
  -webkit-backdrop-filter: blur(20px) saturate(1.6);
  backdrop-filter: blur(20px) saturate(1.6);
}
.aec-window.aec-head-shadow .aec-head::after { opacity: 1; }
/* (the hairline between Human help and Menu is REMOVED — Paul 260730.
   It read as a vertical rule floating in the header on desktop; the gap
   already separates them. The element is gone from the JS too, not
   merely hidden.) */
/* one gap value everywhere in the header row (Paul 260723 #7) */
.aec-headbtns { display: flex; gap: 6px; flex: 0 0 auto; }
.aec-headbtns .aec-iconbtn { flex: 0 0 auto; }
.aec-iconbtn {
  min-width: 36px; height: 38px; border: 1px solid var(--aec-glass-line);
  background: transparent;
  transition: height .18s ease, min-width .18s ease, transform .09s ease,
              background-color .12s ease;
  border-radius: 9px; color: var(--aec-charcoal-soft);
  font-size: 17px; line-height: 1; font-weight: 600;
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; padding: 0 6px;
}
.aec-iconbtn:hover { background: var(--aec-ivory-2); }
/* (the top-left collapse chevron was removed 260721d #2 — unclear and
   messy; the labelled bottom-right Hide button minimises on mobile) */
/* the word "Menu" shows EVERYWHERE (Paul 260723 #1 — desktop had a
   bare ☰) */
.aec-menu-label { display: inline; font-size: 13.5px; font-weight: 600; }
/* tighter (Paul 260813) — the bars and the word were carrying 7px of
   air each side plus the icon button's own 6px */
.aec-btn-menu { padding: 0 4px; gap: 5px; white-space: nowrap; width: auto; }
.aec-menu-bars { display: inline-flex; align-items: center; }
.aec-menu-bars svg { width: 17px; height: 17px; display: block; }
@media (max-width: 640px) {
  .aec-head { padding: 0 var(--aec-gutter); }
  .aec-btn-max { display: none; }   /* (the '–' is gone, 260809d) */
}

/* chips — a second glass strip under the header (the direct child
   only; inline chip rows inside the log keep normal flow) */
.aec-window > .aec-chips {
  position: absolute; top: var(--aec-head-h); left: 0; right: 0; z-index: 4;
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 8px var(--aec-gutter);
  background: var(--aec-glass);
  -webkit-backdrop-filter: blur(12px) saturate(1.4);
  backdrop-filter: blur(12px) saturate(1.4);
  border-bottom: 1px solid var(--aec-glass-line);
}
@media (max-width: 640px) {
  .aec-window > .aec-chips {
    top: calc(var(--aec-head-h) + env(safe-area-inset-top)); }
}
/* §11 — the layout is unchanged; only the contrast drops. The SELECTED
   chip is a warm sand fill (no border at all, so it reads as chosen
   rather than outlined); the others are quiet surfaces behind a
   hairline. Nothing becomes colourful. */
.aec-chip {
  /* ~10% smaller (Paul 260723 #3) */
  font-size: 12.5px; font-weight: 500; padding: 6px 13px;
  border-radius: 8px;   /* the bubble's corner (Paul 260821d) */ border: 1px solid transparent;
  background: var(--aec-chip-on-bg); color: var(--aec-chip-on-fg);
}
.aec-chip.aec-chip-action {
  background: var(--aec-surface); border-color: var(--aec-line);
  color: var(--aec-charcoal); cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease,
              transform .09s ease;
}
.aec-chip.aec-chip-action:hover {
  background: var(--aec-ivory-2); border-color: var(--aec-accent-line);
}

/* ── yes/no quick replies (Paul 260808) ─────────────────────────────
   Two buttons under a reply that asked a yes/no question. Same family
   as the chips and the on-page actions — surface, hairline, the one
   primary on hover — because they are the same kind of object: a small,
   optional thing to tap that must read as Ellie offering rather than
   the page insisting.

   Deliberately NOT a call-to-action pair: no colour on Yes to make it
   the "right" answer, no fill, no shadow. A leading question is a
   design decision as much as a wording one, and this one declines to
   lead. Typing the answer instead is always available and never
   discouraged — the composer is untouched while these are on screen. */
.aec-quick {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin: 2px 0 10px 0; padding: 0 var(--aec-gutter);
  /* buttons arrive smoothly, never in a flash (Paul 260821 #5) — the
     same quiet entrance the messages themselves make */
  animation: aec-soft-in .26s ease-out both;
}
/* One entrance for everything that joins the conversation after the
   words: quick answers, on-page actions, product cards, colour
   swatches. They already wait for the reveal to finish; this is the
   other half of the 260821 ruling — arriving gently once they may. */
@keyframes aec-soft-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
@keyframes aec-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
#aiellie-root .aec-quick__btn {
  font: inherit; font-size: 13px; font-weight: 500;
  min-height: 40px; padding: 9px 20px;
  /* the bubble's own corner (Paul 260821d: buttons and product bits
     match the text — the pills were "much too rounded") */
  border-radius: 8px; border: 1px solid var(--aec-line);
  background: var(--aec-surface); color: var(--aec-charcoal);
  cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease,
              color .12s ease;
}
#aiellie-root .aec-quick__btn:hover {
  background: var(--aec-primary-bg); border-color: var(--aec-primary-bg);
  color: var(--aec-primary-fg);
}
#aiellie-root .aec-quick__btn:focus-visible {
  outline: 2px solid var(--aec-focus); outline-offset: 2px;
}
@media (max-width: 640px) {
  #aiellie-root .aec-quick__btn { min-height: 44px; padding: 11px 22px; }
}
/* An ANSWERED row (Paul 260821 #6). The chosen button keeps the primary
   colour so the row records which answer was given; the others step
   back; and after a beat the whole set shrinks to half size and stays
   in the chat — a record of what was offered, not a control any more.
   The transitions live on the answered state so hovering a live button
   is untouched. */
#aiellie-root .aec-quick--answered .aec-quick__btn {
  cursor: default; opacity: .55;
  transition: font-size .3s ease, padding .3s ease, min-height .3s ease,
              opacity .3s ease;
}
#aiellie-root .aec-quick--answered .aec-quick__btn:hover {
  background: var(--aec-surface); border-color: var(--aec-line);
  color: var(--aec-charcoal);
}
/* The chosen mark is the welcome-primary sand, not the ink fill (Paul
   260821e: "the selected colour can be more gentle") — the same 10%
   step off the surface that already means "the chosen one" on the
   welcome primary, read against the others' faded .55. */
#aiellie-root .aec-quick--answered .aec-quick__btn--chosen,
#aiellie-root .aec-quick--answered .aec-quick__btn--chosen:hover {
  background: var(--aec-welcome-primary);
  border-color: var(--aec-welcome-primary-line);
  color: var(--aec-charcoal); opacity: 1;
}
/* half the 40px pill: half the height, half the padding, and the type
   only as far down as it stays readable */
#aiellie-root .aec-quick--small { gap: 5px; }
#aiellie-root .aec-quick--small .aec-quick__btn {
  min-height: 20px; padding: 3px 10px; font-size: 11px;
}

/* ── on-page action buttons (260803) ────────────────────────────────
   "Take me there", "Show me". Same family as the chips above — surface,
   hairline, warm neutrals, the one primary on hover — because they are
   the same kind of object: a small, optional thing to tap. They must
   read as Ellie offering, never as the page shouting.

   Three states, and a button never leaves the one it lands in: busy
   while the browser works, done when it worked, failed when it did not.
   A finished button stays disabled — one action, one outcome. */
.aec-page-actions {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin: 2px 0 10px 0; padding: 0 var(--aec-gutter);
  animation: aec-soft-in .26s ease-out both;   /* Paul 260821 #5 */
}
#aiellie-root .aec-pa-btn {
  font: inherit; font-size: 13px; font-weight: 500;
  /* 40px on a mouse, 44px under a thumb (below) — comfortably tappable
     without turning into a call-to-action bar */
  min-height: 40px; padding: 9px 16px;
  border-radius: 8px;   /* the bubble's corner (Paul 260821d) */ border: 1px solid var(--aec-line);
  background: var(--aec-surface); color: var(--aec-charcoal);
  cursor: pointer; text-align: left;
  transition: background-color .12s ease, border-color .12s ease,
              color .12s ease, opacity .12s ease;
}
#aiellie-root .aec-pa-btn:hover:not(:disabled) {
  background: var(--aec-primary-bg); border-color: var(--aec-primary-bg);
  color: var(--aec-primary-fg);
}
#aiellie-root .aec-pa-btn:disabled { cursor: default; }
#aiellie-root .aec-pa-btn[data-state="busy"] { opacity: .7; }
#aiellie-root .aec-pa-btn[data-state="done"] {
  background: var(--aec-chip-on-bg); border-color: transparent;
  color: var(--aec-chip-on-fg); opacity: 1;
}
#aiellie-root .aec-pa-btn[data-state="done"] .aec-pa-label::after {
  content: " ✓";
}
#aiellie-root .aec-pa-btn[data-state="failed"] {
  color: var(--aec-danger); border-color: var(--aec-danger);
  background: var(--aec-surface); opacity: 1;
}
@media (max-width: 640px) {
  #aiellie-root .aec-pa-btn { min-height: 44px; padding: 11px 18px; }
}

/* The page-side mark. This lands on the SHOP's markup, outside
   #aiellie-root, so it cannot use the widget's theme variables — the
   ring is a dark line inside a light halo, which reads on a white page
   and on a black one. Box-shadow only: no border, no padding, no margin,
   so nothing on the customer's page moves by a pixel. */
.aec-page-highlight {
  box-shadow: 0 0 0 2px rgba(26, 25, 23, .85),
              0 0 0 5px rgba(250, 248, 244, .92),
              0 0 0 7px rgba(26, 25, 23, .25) !important;
  border-radius: 6px;
  animation: aec-page-highlight-in .35s ease-out both;
}
@keyframes aec-page-highlight-in {
  from { box-shadow: 0 0 0 0 rgba(26, 25, 23, 0); }
}
@media (prefers-reduced-motion: reduce) {
  /* the mark stays — it is the answer to "where is it?" — but it
     arrives instantly instead of growing */
  .aec-page-highlight { animation: none; }
}

/* messages */
.aec-logwrap { flex: 1 1 auto; position: relative; min-height: 0; }
/* messages fade as they slide beneath the sticky header — decorative
   only (pointer-events none, plain gradient: no mask/backdrop needed,
   works in every browser). Appears ONLY once the conversation has been
   scrolled, so the first message is never dimmed at the top. */
.aec-logwrap::before {
  content: ""; position: absolute; top: var(--aec-head-h); left: 0; right: 0;
  height: 28px; z-index: 2; pointer-events: none;
  background: linear-gradient(to bottom, var(--aec-ivory), transparent);
  opacity: 0; transition: opacity .15s ease;
}
.aec-logwrap.aec-scrolled::before { opacity: 1; }
.aec-log {
  position: absolute; inset: 0; overflow-y: auto;
  /* Hold the scrollbar's space ALWAYS. Without this the bar appears
     the moment the log outgrows the panel, every bubble narrows by its
     width, and a reply being written mid-reveal re-wraps — text moving
     after it has been read, which is the one thing the reveal must
     never do (260726). */
  scrollbar-gutter: stable;
  /* the bottom pad only has to clear the composer — the 16px on top
     of it was dead air on a tall phone, where the screen is mostly the
     gap between the shortcuts and the box (Paul 260813) */
  padding: calc(var(--aec-head-h) + 16px) var(--aec-gutter)
           calc(var(--aec-comp-h, 96px) + 6px);
  display: flex; flex-direction: column; gap: 14px;
}
/* Theme typography must not leak decorative shadows into the widget. */
#aiellie-root, #aiellie-root * { text-shadow: none !important; }
/* scrollbar matches the widget and spans only the visible reading
   area — the track margins mirror the header/composer overlays
   (Paul 260723 #2). Webkit pseudos deliberately WITHOUT the standard
   scrollbar-* props: setting those makes Chromium ignore the pseudos
   (and the standard props can't do track margins). */
.aec-log::-webkit-scrollbar { width: 8px; }
.aec-log::-webkit-scrollbar-track {
  background: transparent;
  margin-top: var(--aec-head-h);
  margin-bottom: calc(var(--aec-comp-h, 96px) + 2px);
}
.aec-log::-webkit-scrollbar-thumb {
  background: var(--aec-line); border-radius: 4px;
}
.aec-log::-webkit-scrollbar-thumb:hover {
  background: var(--aec-charcoal-soft);
}
/* flex items in the scroll column must NEVER be crushed to fit — the
   log scrolls instead. Found 260721h: the products row's overflow-x
   zeroes its flex minimum, so in any long conversation the flex
   algorithm collapsed product cards to a 2px sliver. */
.aec-log > * { flex-shrink: 0; }
.aec-log {
  /* NO scroll-behavior:smooth here — it makes every programmatic pin
     animate, so scrollTop lags the target, the "near bottom?" guard
     misreads mid-flight, and the pin lands short (the half-cut
     thinking box, Paul 260720/21). Deliberate glides pass an explicit
     behavior:'smooth'. */
}
.aec-has-chips .aec-log { padding-top: calc(var(--aec-head-h) + 58px); }
@media (max-width: 640px) {
  .aec-log { padding-top: calc(var(--aec-head-h)
             + env(safe-area-inset-top) + 16px); }
  .aec-has-chips .aec-log { padding-top: calc(var(--aec-head-h)
             + env(safe-area-inset-top) + 58px); }
}
.aec-msg {
  display: flex; flex-direction: column;
  inline-size: fit-content;
  min-inline-size: min(144px, 80%);
  max-inline-size: 80%;
  animation: aec-msg-in .18s ease-out both; }
@keyframes aec-msg-in {
  /* premium entrance: a longer rise, same quick fade (260722d #8) */
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}
/* §9 — messages breathe. 14px → 20px, so the conversation never reads
   as a dense feed; the gap is what separates speakers now that the
   outlines have gone. */
.aec-msg + .aec-msg { margin-top: 0; }
/* THE rule (Paul 260802, flipped): AI Ellie / humans LEFT, customer
   RIGHT — the shop speaks from the left, you answer from the right,
   which is the way every messenger the customer already uses reads. */
.aec-msg.aec-from-agent { align-self: flex-start; align-items: flex-start; }
.aec-msg.aec-from-customer { align-self: flex-end; align-items: flex-end; }
.aec-msg.aec-from-agent,
.aec-msg.aec-from-customer { width: fit-content; }
/* name tag — its own borderless pill in a contrast tone, sitting ON
   TOP of the bubble and slightly overlapping its top edge (Paul
   260721g #2 flipped the stacking). §5 keeps this for the CUSTOMER —
   explicitly NOT an avatar circle, NOT initials — and drops it from
   every AI reply after the first (the JS adds .aec-unlabelled; from
   then on the layout says who is speaking). */
.aec-msg-author {
  position: relative; z-index: 0;
  font-family: var(--aec-display);
  font-size: 11.5px; line-height: 1.2; font-weight: 500;
  color: var(--aec-charcoal-soft);
  background: var(--aec-tag-customer);
  border: 0; border-radius: 5px;
  padding: 2px 9px 5px;
  margin: 0 0 4px 0;
  box-shadow: none;
  inline-size: fit-content; max-inline-size: min(180px, 80%);
  white-space: nowrap;
  display: inline-flex; align-items: center; gap: 4px;
}
.aec-from-agent .aec-msg-author {
  background: var(--aec-bubble-agent-line);
  align-self: flex-start;
}
.aec-from-customer .aec-msg-author { align-self: flex-end; }
.aec-msg-author-text {
  min-inline-size: 0; overflow: hidden; text-overflow: ellipsis;
}
.aec-msg-author-bot { display: inline-flex; flex: 0 0 auto; color: inherit; }
.aec-msg-author-bot svg { display: block; width: 14px; height: 14px; }
.aec-from-agent.aec-human .aec-msg-author {
  background: var(--aec-sage-tag);
}
/* an AI reply that is not the first one carries no tag at all — the
   element stays in the DOM (screen readers still announce the author)
   but takes no space and shows nothing */
.aec-msg.aec-unlabelled > .aec-msg-author {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; margin: 0; padding: 0;
}
/* Time and delivery form one quiet tab tucked behind the bubble. It is
   outside the message-content box, so even 05:27:52 ✓ cannot widen Yes. */
.aec-msg-stack {
  display: grid;
  grid-template-columns: 10px minmax(0, auto) 10px;
  inline-size: 100%; max-inline-size: 100%; min-inline-size: 0;
}
.aec-msg-stack > .aec-msg-bubble {
  grid-column: 1 / -1; grid-row: 1;
  inline-size: 100%; min-inline-size: 0; max-inline-size: 100%;
  box-sizing: border-box;
}
.aec-msg-meta {
  position: relative; z-index: 0;
  grid-column: 1 / -1; grid-row: 2;
  justify-self: start;
  display: inline-flex; align-items: center; gap: 4px;
  inline-size: max-content; max-inline-size: 100%;
  min-height: 18px; box-sizing: border-box;
  margin-top: 3px;
  padding: 0;
  border-radius: 0;
  background: transparent;
  color: var(--aec-time);
  white-space: nowrap;
}
.aec-from-customer .aec-msg-meta { justify-self: end; }
.aec-msg-time {
  font-size: calc(10px * var(--aec-font-scale, 1));
  line-height: 1; font-weight: 400; color: inherit;
  font-variant-numeric: tabular-nums;
  /* a small natural pause after the reply lands, then a gentle fade —
     always the last thing to arrive (260722e #4) */
  animation: aec-time-in .4s ease .3s both;
}
@keyframes aec-time-in { from { opacity: 0; } to { opacity: 1; } }
.aec-msg-bubble {
  position: relative;   /* anchors the "Copied ✓" toast */
  z-index: 1;           /* overlaps the name-tag pill's lower half */
  /* §13 generous corners, §12 paper-on-paper shadow, §6 no border at
     all. The 260722 gradient is GONE (§1): the customer's bubble is
     pure white, Ellie's is a flat warm beige — a gradient inside a
     bubble is a device, and it made both look printed rather than laid
     down.
     The customer's fill has its OWN token rather than --aec-surface:
     dark mode had to lift the bubble clear of the page (260802) and
     --aec-surface is also every card, menu, drawer and input, which
     were not the complaint. */
  padding: 9px 13px; border-radius: 8px;
  line-height: 1.6;
  border: 0;
  background: var(--aec-bubble-customer);
  box-shadow: none;
  white-space: pre-wrap; word-break: normal; overflow-wrap: break-word;
}
.aec-from-customer .aec-msg-bubble {
  box-sizing: border-box;
  border: .5px solid var(--aec-bubble-customer-line);
}
/* A one-word customer reply bottomed out at roughly 70px — narrower
   than the timestamp tab tucked behind it. Longer messages keep their
   content-driven width; this is only a floor.

   It does NOT live on the bubble (260820). There the declaration won
   the cascade and did nothing: `min(144px, 100%)` resolves its
   percentage against the bubble's containing block, which is a grid
   area inside a `fit-content` stack — indefinite, so the percentage
   collapsed to the content width and the min never applied. Measured
   in a real browser: "Yes" came out at 70.7px with `getComputedStyle`
   cheerfully reporting `min(150px, 100%)`.

   It sat on `.aec-from-customer .aec-msg-stack` until the 260821 width
   round flattened that layer, and now lives on `.aec-msg` (above).
   Either way the percentage resolves against the log, which has a real
   width, so the floor holds and a narrow screen still cannot be
   overflowed. */
/* touch devices: long-press = copy (the native selection callout would
   fight the gesture); desktop keeps normal text selection */
@media (pointer: coarse) {
  .aec-msg-bubble {
    -webkit-touch-callout: none;
    -webkit-user-select: none; user-select: none;
  }
}
.aec-copied {
  position: absolute; top: -32px; right: 6px; z-index: 3;
  background: var(--aec-charcoal); color: var(--aec-ivory);
  font-size: 12.5px; font-weight: 600; padding: 4px 11px;
  border-radius: 12px; box-shadow: var(--aec-shadow);
  pointer-events: none; white-space: pre-line;
  transition: opacity .4s ease;
}
.aec-copied-out { opacity: 0; }
/* word-by-word reveal: each word fades in softly at reading pace */
/* Per-CHARACTER fade (Paul 260726): "each letter should fade in, just
   enough to make it smooth." 140ms linear is deliberately barely
   perceptible — it softens the leading edge without the shimmer the old
   .45s per-WORD fade produced. Only the few characters still fading
   carry this class; the JS merges them into plain text afterwards, so
   this never animates more than a handful of nodes at once.
   Reduced-motion users get the text with no animation at all. */
/* Streaming flow (Paul 260726). Each line is a NON-WRAPPING block whose
   break point was measured before it was revealed, so nothing re-wraps
   and no character can move once it has appeared. The animated height
   lives on .aec-flow and only ever grows at the bottom edge; only one
   upcoming line is ever reserved, so the empty shape of the full answer
   is never exposed. Width is untouched — the bubble is already a fixed
   share of the panel. */
.aec-msg-bubble .aec-flow {
  overflow: hidden;
  transition: height .14s cubic-bezier(.25,.8,.3,1);
  will-change: height;
}
.aec-msg-bubble .aec-flow-in { display: block; }
.aec-msg-bubble .aec-para { display: block; }
.aec-msg-bubble .aec-para + .aec-para { margin-top: .75em; }
.aec-msg-bubble .aec-line {
  display: block;
  white-space: pre;          /* the break is already decided */
}
/* hidden measuring twin: inherits the bubble's typography, contributes
   no height, and is what Range.getClientRects() is read from */
.aec-msg-bubble .aec-mirror {
  position: absolute; visibility: hidden; pointer-events: none;
  height: 0; overflow: hidden; top: 0; left: 0;
  /* MUST match the bubble's own wrapping (white-space + word-wrap
     above), or the measured breaks would not be the ones the finished
     message settles into. Links keep the `anywhere` rule they get from
     .aec-msg-bubble a, here as well. */
  white-space: pre-wrap; word-wrap: break-word;
}
.aec-msg-bubble .aec-ch { animation: aec-ch-in .14s linear both; }
@keyframes aec-ch-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .aec-msg-bubble .aec-ch { animation: none; }
  /* no animated height either — the JS also switches to grouped reveals */
  .aec-msg-bubble .aec-flow { transition: none; }
}
.aec-msg-bubble a, .aec-receipt-body a {
  color: var(--aec-ink); font-weight: 600;
  text-decoration: underline; text-underline-offset: 2px;
  overflow-wrap: anywhere; /* raw tracking URLs wrap, never overflow */
}
/* speech tails (Paul 260802, replacing the 260721h tightened bottom
   corner): three corners keep the generous 24px curve; the TOP corner
   on the speaker's own side is squared off and grows a small beak that
   runs out past the bubble edge and tapers back in — the start of a 💬
   tail, drawn at the corner rather than hanging off the bottom.
   The beak is a clipped pseudo-element inheriting the bubble's own
   fill, so it follows both themes and every bubble variant for free. */
.aec-from-agent .aec-msg-bubble::before,
.aec-from-customer .aec-msg-bubble::before { content: none; }
/* Ellie/human on the left: the beak points out to the LEFT */
.aec-from-agent .aec-msg-bubble { border-top-left-radius: 8px; }
.aec-from-agent .aec-msg-bubble::before {
  left: -8px;
  clip-path: polygon(0 0, 100% 0, 100% 100%);
}
/* the customer on the right: the beak points out to the RIGHT */
.aec-from-customer .aec-msg-bubble { border-top-right-radius: 8px; }
.aec-from-customer .aec-msg-bubble::before {
  right: -8px;
  clip-path: polygon(0 0, 100% 0, 0 100%);
}
/* (the human colleague's beak used to be drawn twice — a sage edge in
   ::before and the fill laid back 1px inside it in ::after — purely to
   carry the bubble's hairline round the corner. The hairline is gone
   (Paul 260818), so the beak is the ordinary one above: `inherit` picks
   up the sage fill in both themes for free.) */
/* Ellie's bubbles hold a STABLE width (Paul 260726 acceptance: "no
   bubble-width changes" during streaming). They used to shrink-wrap to
   content — align-self:flex-end makes the message column content-sized —
   so a streamed reply widened with every line until it hit the cap, and
   worse, the line-break measurement was taken at whatever width the
   bubble happened to be at that instant.

   A reply BEING WRITTEN therefore takes its full share and holds it,
   which is the only width knowable at the first character: the text it
   will settle at has not arrived yet. Left as fit-content, the column
   measures against the handful of characters on screen, bottoms out at
   the 144px floor — and that floor is the width the line breaks are then
   measured against, so the whole reply wraps at ~150px and SNAPS to its
   real width at the settle (Paul 260821, screenshots). Growth is now
   purely vertical and the measurement is correct from the first
   character.

   Two states carry it, because they overlap rather than follow one
   another: .aec-streaming is the turn bubble from the dots onward, and
   .aec-revealing is on any message a revealer is writing into. The
   revealer adds its class before the first measurement and NEVER removes
   it — adoptStream drops .aec-streaming the moment the final text lands,
   which is typically seconds before the last character is written, and
   the width must not move then either.

   Static and history bubbles keep the balanced fit-content sizing
   (260821 12:53) — they have a real natural width and no measurement
   depending on it. */
.aec-msg.aec-from-agent.aec-streaming,
.aec-msg.aec-from-agent.aec-revealing {
  inline-size: 80%; width: 80%;
}
/* (the separate typing bubble is retired — the dots now open inside
   the reply's own bubble, see .aec-turn-status) */
.aec-from-agent .aec-msg-bubble {
  background: var(--aec-bubble-agent);
  border: 0;
  /* the AI bubble sits BELOW the page in light mode and above it in
     dark, so its shadow would fight the tint — the tint alone is the
     separation (§6) */
  box-shadow: none;
}
/* a real colleague's message keeps an identity of its own — now the
   fill ALONE (Paul 260818: the outline on his bubble goes). Every other
   bubble in the conversation is borderless; his was the one exception,
   and it read as a box round what he said rather than as him. */
.aec-from-agent.aec-human .aec-msg-bubble {
  border: 0;
  background: var(--aec-sage-bg);
  box-shadow: none;
}
.aec-msg-state {
  min-width: 0; min-height: 10px;
  font-size: calc(10px * var(--aec-font-scale, 1)); line-height: 1;
  color: var(--aec-time);
  display: inline-flex; align-items: center; gap: 4px;
}
.aec-msg-state:empty { display: none; }
.aec-delivery-ok { color: var(--aec-time); font-weight: 500; }
.aec-msg-state .aec-fail { color: var(--aec-danger); font-weight: 600; }
.aec-msg-state button {
  border: 0; background: none; color: var(--aec-ink); font-weight: 600;
  text-decoration: underline; font-size: 13px; padding: 2px;
}
.aec-msg-attachment {
  font-size: 13.5px; color: var(--aec-charcoal-soft);
  padding: 3px 4px; font-style: italic;
}

/* jump to latest */
.aec-jump {
  /* floating semi-transparent pill just above the composer, sliding
     in like modern messengers (260722e #3); surface tokens so it
     stays readable in BOTH themes (260720 #5) */
  position: absolute; right: 14px; z-index: 2;
  /* lifted clear of the box edge and near-opaque: floating over text
     it must read as a deliberate control, not a rendering error
     (Paul 260722p screenshot) */
  bottom: calc(var(--aec-comp-h, 96px) + 18px);
  border: 1px solid var(--aec-glass-line); border-radius: 8px;
  background: var(--aec-glass-strong); color: var(--aec-charcoal);
  -webkit-backdrop-filter: blur(12px) saturate(1.4);
  backdrop-filter: blur(12px) saturate(1.4);
  font-size: 13px; font-weight: 600; padding: 6px 12px;
  box-shadow: none;
  animation: aec-jump-in .2s ease both;
}
@keyframes aec-jump-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* receipts — success feedback that reinforces confidence without
   flash (260722e #7): soft rise-in, then one gentle sage settle */
.aec-receipt, .aec-basket-card { animation: aec-msg-in .18s ease-out both; }
.aec-receipt:not(.aec-receipt-fail) {
  animation: aec-msg-in .18s ease-out both, aec-settle 1s ease .25s;
}
@keyframes aec-settle {
  0% { box-shadow: 0 0 0 0 var(--aec-sage-ring); }
  60% { box-shadow: 0 0 0 6px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
/* (the ✓-in-a-disc tick and its aec-tick pop went with the card's
   title row, 260821j — Ellie's sentence is the confirmation now) */
.aec-receipt {
  align-self: stretch; max-width: 100%;
  border: 1px solid var(--aec-line); border-left: 3px solid var(--aec-sage);
  border-radius: var(--aec-radius);
  background: var(--aec-surface); padding: 13px 16px;
  box-shadow: var(--aec-shadow-card);
}
.aec-receipt.aec-receipt-fail { border-left-color: var(--aec-danger); }
.aec-receipt-title { font-weight: 600; font-size: 15px; }
.aec-receipt-body { font-size: 15px; margin-top: 3px; }
.aec-receipt-meta {
  font-size: 13.5px; color: var(--aec-charcoal-soft); margin-top: 5px;
}

/* ── the answer that came back (CP3, 260804) ─────────────────────────
   Deliberately the receipt's shape rather than a new one: it is the same
   kind of object — a settled outcome, not somebody talking. An INTERIM
   update ("still on it") keeps the quiet line; a FINAL one takes the
   emphasis border, because that is the one the customer was waiting
   for. No new colours: this is the existing restrained language. */
.aec-update-card {
  align-self: stretch; max-width: 100%;
  border: 1px solid var(--aec-line);
  border-left: 3px solid var(--aec-line);
  border-radius: var(--aec-radius);
  background: var(--aec-surface); padding: 13px 16px;
  box-shadow: var(--aec-shadow-card);
  animation: aec-msg-in .18s ease-out both;
}
.aec-update-card.aec-update-final { border-left-color: var(--aec-ink); }
.aec-update-head {
  font-size: 13px; font-weight: 600; letter-spacing: .02em;
  text-transform: uppercase; color: var(--aec-charcoal-soft);
}
.aec-update-body { font-size: 15px; margin-top: 4px; }
.aec-update-meta {
  font-size: 13.5px; color: var(--aec-charcoal-soft); margin-top: 5px;
}

/* ── basket / checkout card (§10) ────────────────────────────────────
   Layout unchanged, deliberately: this is the one place in the widget
   where a customer is about to spend money, and it already works.
   Refinements only — softer border, more whitespace, the product
   thumbnail on the right, a BLACK Checkout, View basket secondary,
   Undo text-only. */
/* COMPACT since 260821j (Paul: "excessive repetition and the huge
   card"). Ellie's sentence carries the words; this element is the
   working line while the shop answers, then just the two buttons.
   The thumbnail, product and price rows went with their markup — the
   silent transcript record keeps name and price for the export. */
.aec-basket-card {
  align-self: stretch; position: relative;
  border: 1px solid var(--aec-line);
  border-radius: 8px;   /* the bubble's corner (Paul 260821d) */
  background: var(--aec-surface); padding: 12px 14px;
  box-shadow: var(--aec-shadow-card);
}
/* settled OK: no box at all — a row of buttons under Ellie's sentence,
   the same weight as the quick answers above it */
.aec-basket-card.aec-basket--done {
  border: 0; background: none; box-shadow: none; padding: 2px 0 6px;
}
.aec-basket-line {
  display: flex; align-items: center; gap: 10px;
  font-size: 15px;
}
.aec-basket-line[hidden] { display: none; }
.aec-basket-title { font-weight: 600; }
.aec-basket-title .aec-fail { color: var(--aec-danger); }
.aec-basket-actions {
  display: flex; gap: 10px; margin-top: 12px; align-items: center;
  flex-wrap: wrap;
}
/* buttons only: nothing above them to stand off from */
.aec-basket-card.aec-basket--done .aec-basket-actions { margin-top: 0; }
.aec-basket-actions .aec-undo {
  border: 0; background: none; color: var(--aec-charcoal-soft);
  font-size: 14px; font-weight: 500; text-decoration: underline;
  text-underline-offset: 2px; padding: 6px;
}
@media (max-width: 640px) {
  /* All three sit on ONE row, at their natural widths — as in the
     render. They used to stretch edge-to-edge, which on a 390px phone
     pushed Undo onto a line of its own and dropped the timestamp below
     the card's own thumbnail. */
  .aec-basket-actions .aec-btn {
    flex: 0 1 auto; text-align: center; padding: 11px 16px;
  }
}

/* ── product cards — COMPARISON CARDS (Paul 260822) ───────────────────
   The cards were miniature shop rows: a 40px round thumbnail, the whole
   database name wrapping beside it, and a large black Add button
   carrying most of the ink on a strip that ran the full width of the
   window. Two of them shown "so you can compare" could not be compared
   — the picture, the one thing the customer asked for, was the
   smallest element on the card and the controls were the loudest.

   Inverted here. Reading order down each card is PICTURE → short name →
   specs → price + a small Add, the picture is roughly four times the
   area it was, and the row of cards is no wider than the bubble above
   it so it reads as part of Ellie's message rather than a shop pane
   bolted underneath. Nothing filled, nothing shadowed: a hairline, the
   bubble's 8px corner, and the photograph doing the talking.

   Compact is still the rule (Paul 260806 — "do not place large images
   inside the conversation because this makes the chat chaotic"). Four
   times a thumbnail is a card, not a hero image. */
.aec-products {
  /* the bubble's width, on the bubble's side: two cards and the gap
     land within Ellie's 80% (.aec-msg), so the row belongs to what she
     just said. 420px caps it on a docked/desktop window, where 80% of
     760px would give each card the width of a poster. */
  align-self: flex-start;
  max-width: min(80%, 420px);
  display: flex; gap: 10px;
  overflow-x: auto; padding: 2px 0 6px;
  animation: aec-soft-in .26s ease-out both;   /* Paul 260821 #5 */
}
.aec-product-card {
  /* Two cards SHARE the row; three or more scroll rather than shrink
     into unreadable slivers (the 260721h flex-minimum trap).
     The shrink is 1, not 0, and the floor is min-width — with
     `flex: 1 0 150px` two 150px cards plus the 10px gap came to 310 in
     a 302px row, so the second card was clipped by 8px and lost the
     right edge of its "This one" button. Every geometry assertion
     passed that; the screenshot did not (Paul's rule, 260821). */
  flex: 1 1 150px; max-width: 200px; min-width: 130px;
  transition: border-color .15s ease;
  border: 1px solid var(--aec-line);
  border-radius: 8px;   /* the bubble's corner (Paul 260821d) */
  background: var(--aec-surface); overflow: hidden;
  display: flex; flex-direction: column;
}
.aec-product-card:hover { border-color: var(--aec-accent-line); }

/* The picture, and the whole panel is the enlarge control — Paul asked
   for the image itself to be clickable, with a magnifier in the corner
   and the words underneath. A real <button>, so Enter/Space work and
   the label is read out. */
.aec-product-figure {
  position: relative; display: block; width: 100%;
  margin: 0; padding: 8px 8px 4px; border: 0;
  background: var(--aec-accent);
  font: inherit; color: var(--aec-charcoal-soft);
  cursor: zoom-in; text-align: center;
}
.aec-product-img {
  display: block; width: 100%; aspect-ratio: 1 / 1;
  object-fit: contain;
  /* white behind a cut-out product shot in BOTH themes: these are
     studio photographs on white, and a dark tile behind one shows as a
     grey rectangle around the scarf */
  background: #ffffff; border-radius: 6px;
}
.aec-product-zoom {
  position: absolute; top: 14px; right: 14px;
  width: 26px; height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--aec-glass-strong); color: var(--aec-charcoal);
  border: 1px solid var(--aec-glass-line);
}
.aec-product-zoom svg { display: block; }
/* The words under the picture (Paul 260822b: "perhaps no need for
   literal 'Click to enlarge' if the zoom icon is sufficiently obvious;
   on mobile, a tiny caption may still help"). So: the magnifier
   carries it wherever there is a pointer — a hover and a zoom cursor
   finish the sentence — and the caption appears where there is not.
   Hidden, not deleted: on a phone the icon is the ONLY hint, and this
   is the one place a hover can never explain anything. */
.aec-product-enlarge {
  display: none; padding-top: 5px;
  font-size: 11.5px; line-height: 1.2; color: var(--aec-charcoal-soft);
}
@media (hover: none), (pointer: coarse), (max-width: 640px) {
  .aec-product-enlarge { display: block; }
}
.aec-product-figure:hover .aec-product-enlarge,
.aec-product-figure:focus-visible .aec-product-enlarge {
  color: var(--aec-charcoal);
}
.aec-product-figure:focus-visible {
  outline: 3px solid var(--aec-accent-line); outline-offset: -3px;
}

.aec-product-body { padding: 9px 11px 11px; display: flex;
  flex-direction: column; gap: 5px; flex: 1 1 auto; min-width: 0; }
.aec-product-name { font-size: 14.5px; font-weight: 600; line-height: 1.3; }
/* The external-link mark rides with the last word, never alone on a
   line of its own — the phone screenshot caught it there (260822b),
   where a mark under the name reads as a bullet, not as a link. */
.aec-product-name-text { text-underline-offset: 2px; }
.aec-product-name-tail { white-space: nowrap; }
.aec-product-open {
  display: inline-flex; vertical-align: -1px; margin-left: 4px;
  opacity: .75;
}
.aec-product-open svg { display: block; }
/* How it looks, in Ellie's words — the clause that used to be in the
   paragraph. Full-strength text: it is the reason one card is being
   chosen over the other, not a footnote. */
.aec-product-desc { font-size: 13px; line-height: 1.35; }
.aec-product-spec {
  font-size: 12.5px; line-height: 1.35; color: var(--aec-charcoal-soft);
}
/* minimal card foot (260722z): price left, the card's one control
   right, with a hairline above it — the price and the action are a
   different KIND of thing from the description above them */
.aec-product-foot {
  margin-top: auto; padding-top: 8px;
  border-top: 1px solid var(--aec-line);
  display: flex; align-items: center;
  justify-content: space-between; gap: 8px;
}

/* "This one" (Paul 260822b §6) — the card's own way of saying which
   shade is meant, in place of two buttons under the row repeating two
   names that are already on screen. Quieter than Add on purpose: it
   answers a question, it does not spend money. */
.aec-pickbtn {
  position: relative;
  flex: 0 0 auto; height: 32px; padding: 0 12px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--aec-line); border-radius: 8px;
  background: var(--aec-surface); color: var(--aec-charcoal);
  font: inherit; font-size: 12.5px; font-weight: 600; line-height: 1;
  white-space: nowrap; cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease,
              transform .09s ease, opacity .12s ease;
}
/* the same 44px of finger as the Add control, without the ink */
.aec-pickbtn::after { content: ''; position: absolute; inset: -6px -4px; }
.aec-pickbtn:hover:not(:disabled) {
  background: var(--aec-ivory-2); border-color: var(--aec-accent-line);
}
.aec-pickbtn:focus-visible {
  outline: 3px solid var(--aec-accent-line); outline-offset: 2px;
}
/* Answered: the chosen card keeps its colour and its words, the other
   steps back — the same "the choice stays on record" settle the quick
   replies got (Paul 260821 #6), because the pick sends SILENTLY and
   this row is the only visible record of what the customer said. */
.aec-pickbtn:disabled { cursor: default; opacity: .5; }
.aec-product-card--chosen {
  border-color: var(--aec-accent-line);
  background: var(--aec-accent);
}
.aec-product-card--chosen .aec-pickbtn {
  opacity: 1; background: var(--aec-accent);
  border-color: var(--aec-accent-line); color: var(--aec-charcoal);
}
.aec-product-card--passed { opacity: .62; }
/* Ellie's own recommendation, when she gave a reason for one (the
   260806 "my pick"): a hairline, never a badge — the card is already
   carrying a picture, a name, a description, a price and a control. */
.aec-product-card--pick { border-color: var(--aec-accent-line); }

/* The add control (Paul 260807 → 260808 → 260822) — it replaced "Buy
   now", which claimed something the action never did, then the
   basket-plus ICON that replaced that, which Paul could not read. A
   labelled pill: the trolley carries the meaning at a glance and the
   word carries it for certain.
   OUTLINED and small since 260822: filled black, 44px tall and half
   the card wide, it was the loudest thing on a card whose subject is a
   photograph — "it should occupy only the space its action requires".
   The height comes down to 32px and the fill goes; the TAP TARGET does
   not move, because ::after extends it back past 44px beyond the
   button's edges, where nothing else on the card can be hit. */
.aec-addbtn {
  position: relative;
  flex: 0 0 auto;
  height: 32px;
  padding: 0 10px;
  display: inline-flex; align-items: center; justify-content: center;
  gap: 5px;
  border: 1px solid var(--aec-line);
  border-radius: 8px;   /* the bubble's corner (Paul 260821d) */
  background: var(--aec-surface); color: var(--aec-charcoal);
  font: inherit; font-size: 12.5px; font-weight: 600; line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease,
              transform .09s ease, opacity .12s ease;
}
/* the accessible minimum, kept without the ink: 32px + 6px above and
   below = 44px of finger */
.aec-addbtn::after {
  content: ''; position: absolute; inset: -6px -4px;
}
.aec-addbtn__icon { font-size: 14px; line-height: 1; }
/* The face changes between "Add", "Adding…" and "Added ✓", and a pill
   that resizes under the customer's finger is how a mis-tap happens.
   The widest of the three sets the width once. */
.aec-addbtn__text { min-width: 4.4em; text-align: center; }
.aec-addbtn:hover:not(:disabled) {
  background: var(--aec-ivory-2); border-color: var(--aec-accent-line);
}
.aec-addbtn:active:not(:disabled) { transform: scale(.94); }
/* A visible focus ring, on the button itself rather than only the
   browser default — the widget sits on shop pages whose CSS resets are
   not ours to predict. */
.aec-addbtn:focus-visible {
  outline: 3px solid var(--aec-accent-line);
  outline-offset: 2px;
}
.aec-addbtn:disabled { cursor: default; opacity: .55; }
/* Working and added are said in WORDS on the button ("Adding…",
   "Added ✓"), so nothing here has to animate to carry the state. The
   added state is full-strength green even though the button is still
   disabled — a confirmation must not read as greyed out. */
.aec-addbtn.is-added {
  /* the ONE moment this quiet control is allowed to fill: it is
     confirming that something happened. Its own text goes with it —
     charcoal on sage is not a confirmation, it is a smudge. */
  background: var(--aec-sage-text, #4a7c4e);
  border-color: var(--aec-sage-text, #4a7c4e);
  color: var(--aec-primary-fg); opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .aec-addbtn:active:not(:disabled) { transform: none; }
}

/* The product name links to the product page, and SAYS SO (Paul
   260822): underlined, with the open-page mark beside it. It used to
   underline on hover only — invisible on a phone, where there is no
   hover, so the one way into the product page looked like a heading.
   Two cards is not "a row of links"; the picture above each name is
   what stops it reading as a list. */
.aec-product-link { color: inherit; cursor: pointer;
  text-decoration: none; }
/* The rule sits on the WORDS, not on the anchor: an underline set on
   the anchor is drawn straight through the icon box beside them. */
.aec-product-link .aec-product-name-text {
  text-decoration: underline; text-decoration-thickness: 1px;
  text-decoration-color: var(--aec-accent-line);
}
.aec-product-link:hover .aec-product-name-text {
  text-decoration-color: currentColor;
}
.aec-product-link:focus-visible {
  outline: 2px solid var(--aec-accent-line); outline-offset: 2px;
  border-radius: 3px;
}
.aec-product-price {
  /* chat font, not serif (Paul 260723b #1) */
  font-size: 17px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.aec-product-stock { font-size: 13px; font-weight: 600; }
.aec-product-stock.aec-in { color: var(--aec-sage-text); }
.aec-product-stock.aec-out { color: var(--aec-danger); }
.aec-product-delivery { font-size: 12.5px; color: var(--aec-charcoal-soft); }
.aec-product-card .aec-btn { margin: 8px 11px 11px; padding: 8px 10px; }
@media (max-width: 640px) {
  /* On a phone the bubble's 80% would leave two cards at ~145px each.
     The row is allowed nearly the full column instead — still SIDE BY
     SIDE, which is the point of showing two (Paul 260822 §11) — and
     anything wider than the room scrolls horizontally rather than
     stacking, so the comparison survives. */
  .aec-products { max-width: 94%; }
  .aec-product-card { flex-basis: 46%; }
}

/* The separate full-width progress box that used to sit between the log
   and the composer is GONE (Paul 260726/260727): its rules, its shimmer
   and its step list went with it. Everything a turn has to say now
   happens inside the reply's own bubble — see .aec-turn-status below. */
.aec-typing { display: inline-flex; gap: 4px; align-items: center; }
.aec-typing i {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--aec-ink); display: inline-block;
  animation: aec-dot 1.2s ease-in-out infinite;
}
.aec-typing i:nth-child(2) { animation-delay: .15s; }
.aec-typing i:nth-child(3) { animation-delay: .3s; }
@keyframes aec-dot {
  0%, 60%, 100% { transform: translateY(0); opacity: .5; }
  30% { transform: translateY(-4px); opacity: 1; }
}
/* ── the turn's status area (Paul 260726 "one bubble", 260727 "one
   thought replacing the other") ──────────────────────────────────────
   Inside the reply's own bubble, above the text, there is a SINGLE line
   with exactly one occupant: the dots while Ellie thinks, then the
   thought that is actually running, then the next thought — each
   cross-fading over the last in place. It folds away as the first words
   are written. Nothing is created or destroyed around it, nothing
   accumulates, and the only movement is this one line changing height
   when a thought needs two lines. */
.aec-turn-status {
  overflow: hidden;
  /* EXACTLY the reveal's growth timing and curve (.14s, same easing as
     .aec-flow) — not a nicer-looking curve of its own. The fold and the
     text area's first growth start in the same frame and move in
     opposite directions, so if their timings differ the bubble grows
     past where it belongs and shrinks back: measured 260727 on staging,
     +37px in ~75ms then −10px as the slower fold caught up. Matched,
     the two cancel and the bubble only ever grows. */
  transition: height .14s cubic-bezier(.25,.8,.3,1), opacity .12s linear;
}
/* the one slot. Occupants are absolutely positioned so an outgoing and
   an incoming thought overlap exactly instead of pushing each other
   around; the slot's own height carries the change. */
.aec-status-line {
  position: relative; overflow: hidden;
  min-height: 20px;
  transition: height .19s cubic-bezier(.4,0,.2,1);
}
.aec-status-item {
  position: absolute; left: 0; right: 0; top: 0;
  display: flex; flex-direction: column; gap: 2px;
  justify-content: center; min-height: 20px;
  color: var(--aec-charcoal); font-weight: 600;
  transition: opacity .19s linear, transform .19s cubic-bezier(.4,0,.2,1);
}
/* a thought rises in from below and the one it replaces leaves upward —
   the same direction of travel as the words that eventually replace
   them both */
.aec-status-item.aec-status-in { opacity: 0; transform: translateY(5px); }
.aec-status-item.aec-status-out { opacity: 0; transform: translateY(-5px); }
.aec-status-label { letter-spacing: -.01em; }
/* a thought whose work has finished settles where it is — ticked and
   eased back — and waits there for the next thought or for the words.
   It never reverts to the dots: those are what a turn OPENS with. */
.aec-status-item.aec-status-done {
  color: var(--aec-charcoal-soft); font-weight: 500;
}
.aec-status-done .aec-status-label::after {
  content: " ✓"; color: var(--aec-sage-text); font-weight: 700;
}
/* A step that did NOT work used to be marked with a red ✕ here
   (260820c: the tick is the machine saying it happened, so a failed
   send must never wear one). Paul, 260824, on Mary George's chat:
   *"the while thinking messages often have a red X next to them, and a
   lot of them are either irrelevant or confusing. They should only ever
   refer to a specific thing that is happening or just the 3 wavy
   dots."* Both rulings are satisfied by showing NOTHING: the step
   leaves the slot (`removeStatusItem`), and what the failure means is
   Ellie's to say in words. The rules are gone rather than left unused,
   so nothing can quietly start wearing them again. */
.aec-status-hint {
  font-size: 12.5px; font-style: italic; font-weight: 400;
  color: var(--aec-charcoal-soft);
}
.aec-turn-status-out { opacity: 0; }
/* the status is the whole content until the text starts, so it needs
   no bottom gap of its own */
.aec-turn-status + .aec-msg-text:empty { display: none; }
.aec-turn-status:not(.aec-turn-status-out) + .aec-msg-text:not(:empty) {
  margin-top: 10px;
}
@media (prefers-reduced-motion: reduce) {
  .aec-turn-status, .aec-status-line, .aec-status-item { transition: none; }
}
/* welcome spread (260722j/k, re-cut 260812, re-cut 260813): the hello
   word rotates language on a soft fade — the multilingual invitation
   shown, not just told — then one short sentence, then the three jobs.

   260813 is a subtraction. The bubble lost the paragraph that argued
   with the customer's expectations, so it lost the padding that
   paragraph needed; the five buttons became three, of which one is the
   primary; the footing went entirely (its line is the composer's
   placeholder now). What is left is the hierarchy Paul asked for and
   nothing beside it: greeting → three shortcuts → free text.

   The wrapper is a plain column so the bubble keeps its 80% share while
   the buttons run the full width of the log. */
.aec-welcome-wrap {
  display: flex; flex-direction: column; align-items: stretch; gap: 14px;
  animation: aec-msg-in .18s ease-out both;
}
/* the wrapper carries the entrance, so the bubble inside must not play
   it a second time (two 8px rises, 18ms apart, read as a stutter) */
.aec-welcome-wrap .aec-msg { animation: none; }
/* 18px/20px carried a three-line paragraph; the greeting is two short
   lines, and the old padding read as an empty box around them */
.aec-welcome .aec-msg-bubble { padding: 13px 16px; }
/* Ellie's one-time line before the first pictures (Paul 260821 #7).
   An ordinary Ellie bubble — it is her saying it, not the interface —
   just a step quieter than an answer, because it is an aside. */
.aec-img-intro .aec-msg-bubble { font-size: 13px; }
.aec-hello-line {
  font-size: 1.5em; line-height: 1.2; margin-bottom: 5px;
  display: flex; align-items: center; gap: 11px;
}
/* the greeting's mark: sized in em so the Text size control moves it
   with the word, and never squeezed by a long hello ("안녕하세요") */
.aec-hello-bot {
  flex: 0 0 auto; display: inline-flex; color: var(--aec-hello-bot);
}
/* 1.3em put it level with the word; 1.1em makes the greeting itself
   unquestionably the main element (Paul 260813) */
.aec-hello-bot svg { width: 1.1em; height: 1.1em; display: block; }
.aec-hello {
  display: inline-block; font-family: var(--aec-display);
  font-weight: 600;
  color: var(--aec-ink);
}
/* (the waving hand is gone, and so is the language rotation it used to
   sit beside — Paul 260813. The greeting is decided once, from the
   browser's own language.) */
.aec-welcome-claim { font-size: 1em; line-height: 1.4; }

/* Three jobs, in the shape of their own hierarchy (Paul 260813):

     [ 🔍  Help me choose a pashmina ]      ← primary, full width
     [ 🚚 Where's my order? ][ ↩ Return or cancel ]

   The primary is the widget's one primary fill (black on light, cream
   on dark — the same token as Send), because it is the job most people
   open the chat for and the only open-ended one. The other two keep
   the quiet outlined surface every other optional tap in this widget
   uses: still one tap, plainly the alternatives, never a row of
   equal-weight shouting.

   Centred rather than left-aligned with an icon column: with three
   buttons in two sizes there is no shared left edge to line labels up
   against, and a centred label in a full-width pill is what reads as
   a button rather than a list row. */
.aec-welcome-acts {
  display: grid; gap: 9px;
  /* The greeting speaks first; the buttons join it half a second later
     (Paul 260821 #1). The wrap's own entrance carries the bubble in;
     `both` holds the buttons invisible through the delay, then they
     make the same soft entrance everything else makes. The space is
     reserved from the start — the log is empty, and a greeting that
     grows would move the words being read. */
  animation: aec-soft-in .3s ease-out .5s both;
}
/* "Try me:" — the quietest thing on the screen, and closer to the
   buttons than to the greeting so it reads as their label rather than
   as a third line of Ellie's. */
.aec-welcome-tryme {
  font-size: 13.5px; color: var(--aec-charcoal-soft);
  padding-left: 3px; margin-bottom: -2px;
}
/* THREE across since 260820d (return and cancel became their own
   buttons). Equal columns so no label looks like the important one,
   a tighter gap to buy the width back, and the labels are allowed to
   wrap to two lines rather than be cut off on a narrow phone. */
.aec-welcome-acts-sub {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 7px;
}
.aec-welcome-acts-sub .aec-welcome-act {
  flex-direction: column; gap: 5px; padding-inline: 6px;
  text-align: center; hyphens: auto;
}
.aec-welcome-act {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  text-align: center;
  background: var(--aec-surface);
  border: 1px solid var(--aec-line);
  border-radius: 8px;   /* the bubble's corner (Paul 260821d) */
  color: var(--aec-charcoal);
  font-family: inherit; font-weight: 500;
  line-height: 1.25; cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease,
              color .12s ease;
}
/* The primary is a STEP off the same surface, not a different kind of
   object (Paul 260813, second pass — the black fill it shipped with was
   the widget's one primary token, and on a cream screen it shouted).
   It travels WITH the theme: lighter than the page in light mode,
   darker in dark mode, and about 10% off the two secondaries either
   way — measured in HSL lightness against --aec-surface, which is what
   the secondaries are filled with:

     light  #ffffff (L 100%) → #ede7de (L 90%)
     dark   #1e1b18 (L 10.6%) → #3b342e (L 20.6%)

   Ten points of lightness is enough to read as the chosen one at a
   glance and quiet enough to still be furniture. Size, width and the
   heavier label carry the rest of the hierarchy. */
/* Slightly stronger than it was, and no more (Paul 260813: "already
   nearly right"). The 10% fill step is HIS number and is untouched —
   the extra priority is height and label, which cost the secondaries
   nothing. */
.aec-welcome-act-primary {
  min-height: 56px; padding: 13px 16px; font-size: 16px;
  font-weight: 600;
  background: var(--aec-welcome-primary);
  border-color: var(--aec-welcome-primary-line);
  color: var(--aec-charcoal);
}
.aec-welcome-act-primary:hover {
  background: var(--aec-welcome-primary-hover);
  border-color: var(--aec-welcome-primary-hover);
}
.aec-welcome-act-primary .aec-welcome-ico { color: inherit; opacity: .8; }
/* The two secondaries must be optically IDENTICAL, not merely equal
   in the grid (Paul 260813). The grid already gives them the same box;
   what differed was inside it — a truck is a wider mark than a return
   arrow, so the icon-to-label distance moved between them. A fixed
   mark column fixes the distance for both, whatever is drawn in it. */
.aec-welcome-act-sub {
  min-height: 44px; padding: 9px 10px; font-size: 13.5px; gap: 8px;
}
.aec-welcome-act-sub .aec-welcome-ico {
  width: 19px; justify-content: center;
}
/* the secondaries hover a SMALLER step than the gap to the primary, or
   hovering one would make it look like the chosen button */
.aec-welcome-act-sub:hover {
  background: var(--aec-ivory-2); border-color: var(--aec-accent-line);
}
.aec-welcome-act:focus-visible {
  outline: 2px solid var(--aec-focus); outline-offset: 2px;
}
.aec-welcome-ico {
  flex: 0 0 auto; display: flex; align-items: center;
  color: var(--aec-charcoal-soft);
}
/* the secondary marks ride at 17px so the two short labels keep the
   room they need beside them */
.aec-welcome-act-sub .aec-welcome-ico svg { width: 17px; height: 17px; }
/* Below ~360px "Return or cancel" and "Where's my order?" stop fitting
   side by side without breaking mid-word, so they go single file. */
@media (max-width: 359px) {
  .aec-welcome-acts-sub { grid-template-columns: 1fr; }
}
/* An ANSWERED welcome (Paul 260821c: "they should remain, become half
   the size, and show an indication of which one i chose"). Same
   grammar as an answered quick-reply row: the chosen button keeps the
   primary colour as the record of the choice, the others step back,
   and after a beat the whole set shrinks to half height. The
   transitions live on the answered state so a live button's hover is
   untouched. */
.aec-welcome--answered .aec-welcome-act {
  cursor: default; opacity: .55;
  transition: font-size .3s ease, padding .3s ease, min-height .3s ease,
              gap .3s ease, opacity .3s ease;
}
.aec-welcome--answered .aec-welcome-act-sub:hover {
  background: var(--aec-surface); border-color: var(--aec-line);
}
.aec-welcome--answered .aec-welcome-act-primary:hover {
  background: var(--aec-welcome-primary);
  border-color: var(--aec-welcome-primary-line);
}
/* gentle, like the quick replies (Paul 260821e): the sand step, full
   opacity against the faded rest — not the ink fill */
.aec-welcome--answered .aec-welcome-act--chosen,
.aec-welcome--answered .aec-welcome-act--chosen:hover {
  background: var(--aec-welcome-primary);
  border-color: var(--aec-welcome-primary-line);
  color: var(--aec-charcoal); opacity: 1;
}
.aec-welcome--answered .aec-welcome-act--chosen .aec-welcome-ico {
  color: inherit; opacity: .9;
}
/* half the pill heights (56/44 → 28/22), the type only as far down as
   it stays readable, and the gaps pulled in with them */
.aec-welcome--small .aec-welcome-acts { gap: 5px; }
.aec-welcome--small .aec-welcome-acts-sub { gap: 4px; }
.aec-welcome--small .aec-welcome-tryme { font-size: 11px; }
.aec-welcome--small .aec-welcome-act-primary {
  min-height: 28px; padding: 5px 10px; font-size: 12px;
}
.aec-welcome--small .aec-welcome-act-sub {
  min-height: 22px; padding: 3px 6px; font-size: 11px; gap: 4px;
}
.aec-welcome--small .aec-welcome-ico svg,
.aec-welcome--small .aec-welcome-act-sub .aec-welcome-ico svg {
  width: 12px; height: 12px;
}
@media (prefers-reduced-motion: reduce) {
  .aec-msg, .aec-msg-time { animation: none; }
  .aec-typing i { animation: none; }
  .aec-input { transition: none; }
  .aec-mic.aec-mic-on, .aec-jump, .aec-menu, .aec-menu.aec-menu-out,
  .aec-receipt, .aec-basket-card,
  .aec-placeholder i {
    animation: none;
  }
  /* static overlay still reads at full strength without the wave */
  .aec-placeholder i { opacity: 1; }
}
/* (the ✓/✨ step LIST is gone — Paul 260727: one thought replaces the
   other, nothing accumulates. Its ✨ marker went with it: a line that
   is replaced rather than listed needs no bullet to tell it apart.
   Stop/Add/Edit action buttons removed earlier — Paul 260721c #2) */

/* confirm card — a two-phase action the customer must approve, so it
   keeps a firmer edge than the other cards, but in ink, not accent */
.aec-confirm {
  align-self: stretch;
  border: 1px solid var(--aec-charcoal-soft);
  border-radius: var(--aec-radius);
  background: var(--aec-surface); padding: 16px 18px;
  box-shadow: var(--aec-shadow-card);
}
.aec-confirm-title { font-weight: 600; font-size: 16px; }
.aec-confirm-body { font-size: 15px; margin: 6px 0 14px; }
.aec-confirm-actions { display: flex; gap: 10px; }
/* §13 — generous corners everywhere; §12 — buttons are flat, never
   elevated */
.aec-btn {
  border-radius: 8px;   /* the bubble's corner (Paul 260821d) */ padding: 11px 20px; font-size: 16px;
  font-weight: 600; letter-spacing: .005em;
  transition: background-color .12s ease, border-color .12s ease,
              box-shadow .12s ease, transform .09s ease;
  border: 1px solid var(--aec-line); background: var(--aec-surface);
  color: var(--aec-charcoal); min-height: 46px;
  text-decoration: none; display: inline-flex; align-items: center;
  justify-content: center; gap: 6px;
}
.aec-btn:hover { border-color: var(--aec-accent-line);
  background: var(--aec-ivory-2); }
/* THE primary: black on light, cream on dark — the only filled control
   in the widget (§1, §10) */
.aec-btn-primary {
  background: var(--aec-primary-bg); border-color: var(--aec-primary-bg);
  color: var(--aec-primary-fg);
}
.aec-btn-primary:hover {
  background: var(--aec-primary-hover);
  border-color: var(--aec-primary-hover);
}
.aec-btn[disabled] { opacity: .55; cursor: default; }

/* date divider — quiet pill (§2) */
.aec-day-divider {
  align-self: center;
  /* still the quiet pill Paul ruled for (§2 of the 260719 spec); only
     the outline goes, per §6 of this round */
  background: var(--aec-ivory-2); border: 0;
  border-radius: 8px; padding: 4px 13px;
  font-size: 12px; color: var(--aec-charcoal-soft);
}

/* notices */
.aec-notice {
  align-self: center; text-align: center;
  font-size: 13.5px; color: var(--aec-charcoal-soft);
  padding: 2px 10px; max-width: 94%;
}

/* The waiting-status panel (Paul's spec, 260822). Deliberately NOT a
   bubble: a bordered strip in the thread, quiet colours, so a wait
   reads as state rather than as Ellie saying something new. */
.aec-wait {
  align-self: flex-start;
  display: flex; flex-direction: column; gap: 2px;
  margin: 2px 0 4px; padding: 7px 12px;
  max-width: 80%;
  font-size: 13px; line-height: 1.35;
  color: var(--aec-charcoal-soft);
  background: var(--aec-accent);
  border: 1px solid var(--aec-accent-line);
  border-left: 3px solid var(--aec-sage);
  border-radius: 8px;
}
.aec-wait-line { color: var(--aec-charcoal); }
.aec-wait-count {
  font-variant-numeric: tabular-nums;
  color: var(--aec-charcoal-soft);
}
.aec-wait-count:empty { display: none; }
.aec-wait--done {
  border-left-color: var(--aec-sage);
  background: var(--aec-sage-bg);
}
.aec-wait--done .aec-wait-line { color: var(--aec-sage-text); }
/* Answered, therefore finished: the panel fades and leaves rather than
   sitting above Ellie's answer for the rest of the conversation (Paul,
   260823c). Reduced-motion still removes it — the point is that it
   goes, not that it glides. */
.aec-wait { transition: opacity .35s ease, transform .35s ease,
                        max-height .35s ease, margin .35s ease,
                        padding .35s ease; }
.aec-wait--gone {
  opacity: 0; transform: translateY(-4px);
  max-height: 0; margin: 0; padding-top: 0; padding-bottom: 0;
  overflow: hidden; pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .aec-wait { transition: none; }
}
.aec-banner {
  position: absolute; top: var(--aec-head-h); left: 0; right: 0; z-index: 6;
  padding: 8px var(--aec-gutter); font-size: 14px; font-weight: 500;
  background: var(--aec-banner-bg); border-bottom: 1px solid var(--aec-line);
  color: var(--aec-banner-text);
}

/* ── modal — one styled system for every dialog (260721b: native
   confirm() looked nothing like the chat). Frosted backdrop over the
   window; compact card in the widget's own palette and type. */
.aec-modal {
  position: absolute; inset: 0; z-index: 9;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  background: rgba(26, 25, 23, .4);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  animation: aec-modal-in .16s ease-out;
}
.aec-modal-card {
  width: 100%; max-width: 330px; max-height: 82%;
  overflow-y: auto;
  background: var(--aec-surface);
  border: 1px solid var(--aec-line);
  border-radius: 14px;
  box-shadow: 0 18px 50px rgba(20, 18, 15, .35);
  padding: 18px;
  animation: aec-modal-card-in .18s ease-out;
}
.aec-modal-title {
  font-family: var(--aec-display); font-size: 18px; font-weight: 600;
  margin: 0 0 8px; letter-spacing: .01em; color: var(--aec-charcoal);
}
.aec-modal-body {
  font-size: 15px; line-height: 1.5; color: var(--aec-charcoal);
}
.aec-modal-actions {
  display: flex; gap: 9px; justify-content: flex-end; margin-top: 16px;
  flex-wrap: wrap;
}
/* a deliberately stacked action set (260811 #2) — rows sit tight under
   each other and each row fills the card, so the buttons line up */
.aec-modal-actions-row { flex-wrap: nowrap; }
.aec-modal-actions-row + .aec-modal-actions-row { margin-top: 9px; }
.aec-modal-actions-row .aec-btn { flex: 1 1 0; min-width: 0; }
.aec-modal-list {
  display: flex; flex-direction: column; gap: 8px; margin-top: 4px;
}
.aec-modal-list .aec-btn {
  width: 100%; text-align: left; justify-content: flex-start;
  font-weight: 500;
}
/* Previous conversations (Paul 260824: "make the previous conversations
   panel look much better, use similar styling to the menu content").
   It was a plain .aec-btn — "date — preview" run together on one line,
   the same box a Cancel button gets. A conversation is naturally TWO
   things (when, and what it was about), so the card borrows the SAME
   two pieces the rest of the menu ecosystem already uses for exactly
   that split: the icon column from .aec-mi/.aec-mrow (the ⋮ menu row
   this panel opens from carries the same bubble), and the bold-title-
   over-muted-subtitle card from .aec-drawer-item (the drawer this menu
   also opens). Nothing new invented — the same two patterns, applied to
   a list neither of them was written for yet. */
.aec-history-item {
  display: flex; align-items: flex-start; gap: 11px; width: 100%;
  text-align: left; border: 1px solid var(--aec-line);
  background: var(--aec-surface); border-radius: 9px; padding: 11px 13px;
  font: inherit; color: var(--aec-charcoal); cursor: pointer;
}
.aec-history-item:hover { border-color: var(--aec-accent-line);
  background: var(--aec-ivory-2); }
/* nudged down a hair so the icon's optical centre lines up with the
   first line of text rather than the text block as a whole */
.aec-history-item .aec-mi { margin-top: 1px; }
.aec-history-text { display: flex; flex-direction: column; min-width: 0; }
.aec-history-when { font-size: 13.5px; font-weight: 600; }
.aec-history-preview {
  display: block; font-size: 13.5px; font-weight: 400;
  color: var(--aec-charcoal-soft); margin-top: 2px;
}
.aec-modal-card input[type="email"],
.aec-modal-card input[type="text"] {
  width: 100%; padding: 11px 12px; border: 1px solid var(--aec-line);
  border-radius: 9px; font-size: 16px; margin: 10px 0 0;
  background: var(--aec-surface); color: var(--aec-charcoal);
}
.aec-modal-card .aec-small {
  font-size: 13.5px; color: var(--aec-charcoal-soft); margin-top: 4px;
}
.aec-btn-danger {
  background: var(--aec-danger-bg); border-color: var(--aec-danger-bg);
  color: #fff;
}
/* Paul 260811 #1: hovering Delete in light mode made it almost vanish.
   `.aec-btn:hover` is (0,2,0) and `.aec-btn-danger` is (0,1,0), so the
   generic hover won and repainted the button ivory — under its own
   white text. The danger hover now carries the same specificity as the
   rule that was beating it, and states its colour rather than filtering
   it, so the ground darkens away from the text in both themes. */
.aec-btn-danger:hover, .aec-btn-danger:focus-visible {
  background: var(--aec-danger-hover);
  border-color: var(--aec-danger-hover);
  color: #fff;
}
@keyframes aec-modal-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes aec-modal-card-in {
  from { opacity: 0; transform: translateY(10px) scale(.97); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .aec-modal, .aec-modal-card { animation: none; }
}

/* handover panel */
.aec-panel {
  align-self: stretch;
  border: 1px solid var(--aec-line); border-radius: var(--aec-radius);
  background: var(--aec-surface); padding: 13px 15px;
}
.aec-panel h3 {
  margin: 0 0 6px; font-size: 19px; font-family: var(--aec-display);
  letter-spacing: .01em;
}
.aec-panel .aec-avail { font-size: 14.5px; margin-bottom: 10px; }
.aec-avail-dot {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  margin-right: 6px; background: #a49d8e;
}
.aec-avail-available .aec-avail-dot { background: var(--aec-sage); }
.aec-avail-unavailable .aec-avail-dot { background: var(--aec-danger); }
.aec-panel-options { display: flex; flex-direction: column; gap: 8px; }
.aec-panel-options .aec-btn { text-align: left; width: 100%;
  justify-content: flex-start; }
.aec-panel input[type="tel"], .aec-panel input[type="text"],
.aec-panel input[type="datetime-local"] {
  width: 100%; padding: 11px 12px; border: 1px solid var(--aec-line);
  border-radius: 9px; font-size: 16px; margin: 6px 0;
  background: var(--aec-surface); color: var(--aec-charcoal);
}
.aec-panel .aec-small {
  font-size: 13.5px; color: var(--aec-charcoal-soft); margin-top: 4px;
}
.aec-panel label.aec-checkline {
  display: flex; gap: 8px; align-items: center; font-size: 14px;
  margin: 5px 0;
}

/* capabilities drawer — desktop side panel */
/* top-left resize grip (desktop) — the native bottom-right grip is
   pinned against the viewport edge on a bottom-right-anchored window */
.aec-resize-handle {
  position: absolute; left: 0; top: 0; width: 18px; height: 18px;
  cursor: nwse-resize; z-index: 7;
  border-left: 3px solid var(--aec-line);
  border-top: 3px solid var(--aec-line);
  border-top-left-radius: var(--aec-radius);
}
.aec-resize-handle:hover { border-color: var(--aec-focus); }
.aec-window.aec-docked .aec-resize-handle {
  top: 0; bottom: 0; left: -5px; width: 10px; height: auto;
  cursor: col-resize; border: 0; border-radius: 0;
}
.aec-send-spinner {
  width: 8px; height: 8px; box-sizing: border-box;
  border: 1px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: aec-spin .75s linear infinite;
}
.aec-window.aec-docked .aec-resize-handle::after {
  content: ''; position: absolute; top: 0; bottom: 0; left: 4px;
  width: 1px; background: var(--aec-line);
}
.aec-window.aec-docked .aec-resize-handle:hover::after,
.aec-window.aec-docked .aec-resize-handle:focus-visible::after {
  background: var(--aec-focus); width: 2px;
}
.aec-window.aec-docked .aec-log > * { max-width: 760px; }
.aec-window.aec-docked .aec-log > .aec-msg {
  max-width: 80%;
  max-inline-size: 80%;
}
@media (prefers-reduced-motion: reduce) {
  html.aec-dock-active body { transition: none; }
}
@media (max-width: 640px) { .aec-resize-handle { display: none; } }
.aec-drawer {
  position: absolute; left: 0; top: 0; bottom: 0; z-index: 6;
  width: 288px; max-width: 88%;
  background: var(--aec-glass-strong);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  backdrop-filter: blur(18px) saturate(1.5);
  border-right: 1px solid var(--aec-line);
  box-shadow: 8px 0 22px rgba(38, 36, 31, .12);
  transform: translateX(-105%);
  transition: transform .18s ease;
  display: flex; flex-direction: column;
  padding: 14px; overflow-y: auto;
}
.aec-drawer.aec-open { transform: translateX(0); }
.aec-drawer h3 {
  font-family: var(--aec-display); font-size: 20px; margin: 0 0 12px;
  letter-spacing: .01em;
  display: flex; justify-content: space-between; align-items: center;
}
/* a real button, not a bare glyph — the borderless × was invisible
   (Paul 260722) */
.aec-drawer-close {
  border: 1px solid var(--aec-bubble-agent-line);
  background: var(--aec-bubble-agent);
  border-radius: 9px; font-size: 20px; line-height: 1;
  min-width: 38px; min-height: 38px; padding: 4px 10px;
  color: var(--aec-charcoal);
}
.aec-drawer-item {
  color: var(--aec-charcoal);
  text-align: left; border: 1px solid var(--aec-line);
  background: var(--aec-surface); border-radius: 9px; padding: 11px 13px;
  margin-bottom: 8px; font-size: 15px; font-weight: 600;
}
.aec-drawer-item:hover { border-color: var(--aec-accent-line);
  background: var(--aec-ivory-2); }
.aec-drawer-item .aec-drawer-sub {
  display: block; font-size: 13.5px; font-weight: 400;
  color: var(--aec-charcoal-soft); margin-top: 2px;
}
/* mobile: the sideways tab is gone; the drawer becomes a bottom sheet
   opened from the composer toolbar */
@media (max-width: 640px) {
  .aec-drawer-tab { display: none; }
  .aec-drawer {
    top: auto; bottom: 0; left: 0; right: 0; width: 100%; max-width: 100%;
    max-height: 72%;
    border-right: 0; border-top: 1px solid var(--aec-line);
    border-radius: 14px 14px 0 0;
    transform: translateY(105%);
    box-shadow: 0 -8px 22px rgba(38, 36, 31, .15);
  }
  .aec-drawer.aec-open { transform: translateY(0); }
}

/* header Human help — §3: no box, no badge, no status light. A headset
   glyph, the words, and the staffed hours beneath them. It is a route
   to a person, so it is spelled out; it is not the primary action, so
   it carries no fill and no outline. */
.aec-head-human {
  display: inline-flex; align-items: center; gap: 9px;
  border: 0; background: transparent;
  border-radius: 10px; padding: 4px 8px;
  /* a step quieter (Paul 260813): 600/13.5 made the route to a HUMAN
     the boldest thing on a bar whose subject is Ellie. Weight, size
     and the mark all come down ~10–15%; it is still the only labelled
     control up there, so it cannot be missed. */
  font-size: 13px; font-weight: 500; color: var(--aec-charcoal);
  min-height: 38px; flex: 0 0 auto; white-space: nowrap;
  transition: background-color .12s ease, transform .09s ease;
}
.aec-head-human:hover { background: var(--aec-ivory-2); }
.aec-human-icon {
  flex: 0 0 auto; display: inline-flex;
  color: var(--aec-charcoal-soft);
}
.aec-human-icon svg { width: 19px; height: 19px; display: block; }
.aec-human-text {
  display: flex; flex-direction: column; gap: 0; line-height: 1.25;
  align-items: flex-start;
}
/* the hours ALWAYS read as hours. The old "Available" state, in sage,
   was the online indicator §3 removes: a chat that has opened is
   obviously online, and a green word next to a headset says nothing a
   customer needs. Availability still governs what the handover panel
   offers — it just stops being a badge. */
.aec-human-sub {
  font-size: 11px; font-weight: 400; letter-spacing: .01em;
  color: var(--aec-charcoal-soft);
}
@media (max-width: 359px) {
  .aec-head-human { padding: 4px 5px; font-size: 12.5px; gap: 6px; }
  .aec-human-icon { display: none; }
}
@media (max-width: 640px) {
  .aec-iconbtn { min-width: 40px; width: auto; height: 40px; }
}

/* ── the Menu (Paul 260813, rebuilt) ────────────────────────────────
   Three named groups with a hairline between them, one restrained line
   icon per row, and the two value-picking preferences built as the same
   segmented pill. What this buys: the panel answers "what kind of thing
   is this row?" before it is read. Ten equally weighted rows could not.

   It also stops looking like a settings popup borrowed from somewhere
   else — softer corner, quieter shadow, and a notch pointing at the
   Menu button, so it reads as belonging to the control that opened it
   rather than floating over the conversation. */
.aec-menu {
  /* the Text size control scales the menu's own text too, but clamped:
     the full 0.85–1.45 range would overflow rows / shrink below
     readable, so the menu follows within 0.9–1.3 (Paul 260723) */
  --aec-menu-scale: clamp(0.9, var(--aec-font-scale, 1), 1.3);
  position: absolute; right: 10px; top: calc(var(--aec-head-h) + 10px);
  z-index: 8;
  background: var(--aec-glass-strong);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  backdrop-filter: blur(16px) saturate(1.5);
  border: 1px solid var(--aec-glass-line);
  /* the same corner as every other panel in the widget (Paul 260813) */
  border-radius: var(--aec-radius); padding: 6px;
  box-shadow: 0 1px 2px rgba(26, 25, 23, .05),
              0 14px 40px rgba(26, 25, 23, .13);
  display: flex; flex-direction: column;
  min-width: calc(238px * var(--aec-menu-scale));
  max-width: calc(300px * var(--aec-menu-scale));
  /* a small spring on open (260722 #12) */
  transform-origin: top right;
  /* 170ms, decelerating, no overshoot (Paul 260813). The old curve
     ended at 1.12 — a spring, which on a settings panel reads as a
     toy. */
  animation: aec-menu-in .17s cubic-bezier(.16, .84, .34, 1) both;
}
/* the notch: a rotated square wearing the panel's own top-left edges,
   sat under the Menu button. Deliberately NOT a filled triangle — the
   panel is glass, and a solid arrow would be the one opaque thing on
   it. It carries no blur of its own (a backdrop-filter on a rotated
   box smears), which at 9px nobody can see. */
.aec-menu::before {
  content: ""; position: absolute; top: -6px; right: 26px;
  width: 11px; height: 11px; transform: rotate(45deg);
  background: var(--aec-glass-strong);
  border-left: 1px solid var(--aec-glass-line);
  border-top: 1px solid var(--aec-glass-line);
  border-top-left-radius: 3px;
}
@keyframes aec-menu-in {
  from { opacity: 0; transform: scale(.97) translateY(-4px); }
  to { opacity: 1; transform: none; }
}
/* closing mirrors the open — no abrupt disappearance (260722e #5) */
.aec-menu.aec-menu-out {
  animation: aec-menu-out .15s ease both;
  pointer-events: none;
}
@keyframes aec-menu-out {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: scale(.98) translateY(-3px); }
}
/* groups: a rule between them rather than around them — a boxed group
   would put four more borders on the screen.

   The captions are gone (Paul, 260821: *"remove the sections headings.
   they add noting but size"*). They were the 260813 answer to ten
   equally weighted rows and they did that job, but the rule now
   carries the same separation in one pixel instead of three lines, and
   on a panel that opens over a live conversation height is the thing
   it can least afford. The rules get a little more air to make up for
   the labels they lose. */
.aec-menu-group { display: flex; flex-direction: column; gap: 1px; }
.aec-menu-group + .aec-menu-group {
  border-top: 1px solid var(--aec-line);
  margin-top: 9px; padding-top: 9px;
}
/* The build stamp (Paul, 260821: *"at top right, in tiny text, a
   version number and last updated date… very small"*). Top right of
   the panel, out of the reading path entirely: it is there to be
   FOUND, not read.

   In FLOW rather than absolutely placed, deliberately: absolute would
   cost no height and would sit on top of the first row's right edge at
   a large text size, and a diagnostic that can overlap the interface
   is worse than one that costs eleven pixels. It is still selectable —
   the point of a version number is that somebody can read it back. */
.aec-menu-stamp {
  padding: 1px 5px 4px; text-align: right;
  font-size: calc(9.5px * var(--aec-menu-scale, 1));
  line-height: 1.2; letter-spacing: .02em;
  color: var(--aec-charcoal-soft); opacity: .55;
  /* `pre`, not `nowrap`: Paul wrote the format with a double space
     between the version and the date — "v1.0.1  21/8 10:46" — and HTML
     would quietly collapse it to one. */
  font-variant-numeric: tabular-nums; white-space: pre;
}
.aec-menu button {
  border: 0; background: none; text-align: left; padding: 8px 10px;
  font-size: calc(14px * var(--aec-menu-scale, 1));
  border-radius: 10px; color: var(--aec-charcoal);
  transition: background-color .12s ease, transform .09s ease;
}
.aec-menu button:hover { background: var(--aec-ivory-2); }
/* fixed icon column → labels share one vertical line (260722d #6) */
.aec-mrow { display: inline-flex; align-items: center; gap: 11px; }
.aec-mi {
  flex: 0 0 auto; width: calc(20px * var(--aec-menu-scale, 1));
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--aec-charcoal-soft);
}
.aec-mi svg {
  width: calc(19px * var(--aec-menu-scale, 1));
  height: calc(19px * var(--aec-menu-scale, 1)); display: block;
}
/* Delete is the only destructive row in the widget. It reads in the
   danger colour — mark and label together, so the row is legible as
   one thing — and gets no fill, no border and no extra weight: last,
   alone under its rule, is what makes it the exception.

   The red is a DESATURATED one (Paul 260813): the cue has to survive,
   because it is the one row that cannot be undone, but a full-strength
   warning red on a menu nobody has opened in anger is the loudest
   thing on a quiet panel. The confirm dialog keeps the real red. */
.aec-menu .aec-menu-danger,
.aec-menu .aec-menu-danger .aec-mi { color: var(--aec-danger-quiet); }
/* boolean settings are switches, not text states (260722 #8) */
.aec-menu-switchrow {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; width: 100%;
}
/* a switch plus the sentence that explains why the browser is refusing
   to honour it (260811 #8) — one menu entry, two lines */
.aec-menu-entry { display: flex; flex-direction: column; }
.aec-menu-note {
  padding: 0 10px 6px calc(41px * var(--aec-menu-scale, 1)); margin: 0;
  font-size: calc(12.5px * var(--aec-menu-scale, 1)); line-height: 1.35;
  color: var(--aec-charcoal-soft);
}
.aec-switch {
  flex: 0 0 auto; width: 40px; height: 24px; border-radius: 12px;
  background: var(--aec-line); position: relative;
  transition: background-color .18s ease;
}
.aec-switch::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--aec-surface);
  box-shadow: 0 1px 2px rgba(20, 18, 15, .25);
  transition: transform .18s ease;
}
[aria-checked="true"] > .aec-switch { background: var(--aec-primary-bg); }
[aria-checked="true"] > .aec-switch::after { transform: translateX(16px); }
/* the two preferences that pick a VALUE share one control shape: a
   label on the left, a segmented pill hard against the right edge —
   the same right edge the switches above end on. */
.aec-menu-ctrlrow {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 5px 10px;
}
.aec-menu-ctrllabel {
  flex: 1 1 auto; min-width: 0;
  font-size: calc(14px * var(--aec-menu-scale, 1));
  color: var(--aec-charcoal);
}
.aec-seg {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 2px; padding: 2px;
  background: var(--aec-ivory-2); border-radius: 10px;
}
.aec-menu .aec-segbtn {
  padding: 5px 10px; border-radius: 8px;
  font-size: calc(12.5px * var(--aec-menu-scale, 1));
  font-weight: 600; color: var(--aec-charcoal-soft);
  text-align: center;
  transition: background-color .12s ease, color .12s ease,
              transform .09s ease;
}
.aec-menu .aec-segbtn.aec-seg-on {
  background: var(--aec-seg-raised); color: var(--aec-charcoal);
  box-shadow: 0 1px 2px rgba(38, 36, 31, .14);
}
/* A− │ 115% │ A+ is ONE pill (Paul 260813): the reading is a cell of
   the control, not a number floating between two buttons.

   The two ENDS carry the raised cell, not the middle. Appearance's
   raised cell means "this is the one you chose"; here nothing is
   chosen — two cells are pressable and the middle one is a readout —
   so raising the ends is what tells the two apart at a glance. The − and
   + keep a 40px target inside the pill. */
.aec-menu .aec-fontbtn {
  min-width: calc(40px * var(--aec-menu-scale, 1));
  font-size: calc(14px * var(--aec-menu-scale, 1));
  padding: 5px 6px;
  background: var(--aec-seg-raised); color: var(--aec-charcoal);
  box-shadow: 0 1px 2px rgba(38, 36, 31, .14);
}
.aec-menu .aec-fontbtn[disabled] {
  opacity: .38; cursor: default; box-shadow: none;
}
.aec-menu .aec-fontbtn:hover:not([disabled]) {
  background: var(--aec-ivory); color: var(--aec-charcoal);
}
/* Compact is a LOOK, not a licence to shrink the target (Paul 260721d
   #1 still stands): under a thumb every cell keeps 40px, which makes
   the pill exactly as tall as the switch rows above it. A mouse does
   not need it, and the desktop panel stays tight. */
@media (pointer: coarse) {
  .aec-menu .aec-segbtn { min-height: 40px; }
}
.aec-fontval {
  min-width: calc(46px * var(--aec-menu-scale, 1)); text-align: center;
  font-size: calc(12.5px * var(--aec-menu-scale, 1));
  font-weight: 600;
  color: var(--aec-charcoal-soft); font-variant-numeric: tabular-nums;
}

/* composer */
.aec-composer {
  /* floats OVER the log like the header does (260722g): glass only
     reads as glass when messages actually scroll beneath it. The log
     reserves --aec-comp-h (kept current by a ResizeObserver) so
     content can always scroll clear. */
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 5;
  /* no dividing line, no gutters (260722m): the input box runs
     edge-to-edge and sits flush on the bottom. The composer itself
     paints NOTHING — the box carries the glass, and the ::before
     ramp below fades the blur in so there is no hard boundary
     (Paul 260722o). */
  padding: 0;
}
/* (the 14px pre-box blur ramp is removed — Paul 260722s: text goes
   from fully visible straight to the box's heavy blur at its edge) */
.aec-attach-chips { display: flex; flex-wrap: wrap; gap: 5px;
  padding: 0 12px 5px; }
/* when attachments are staged the chips row needs its own glass —
   the composer no longer paints a background */
.aec-attach-chips:not(:empty) {
  background: var(--aec-glass-head);
  -webkit-backdrop-filter: blur(12px) saturate(1.5);
  backdrop-filter: blur(12px) saturate(1.5);
  padding-top: 6px;
}
.aec-attach-chip {
  font-size: 13.5px; background: var(--aec-ivory-2);
  border: 1px solid var(--aec-line); border-radius: 11px;
  padding: 4px 10px; display: inline-flex; gap: 7px; align-items: center;
}
.aec-attach-chip button { border: 0; background: none; padding: 0 2px;
  font-size: 15px; }
.aec-composer-row { display: flex; }
/* native-app composer (Paul 260722, supersedes the labelled pills +
   right stack): the wrapper is the ONE visual box — borderless
   textarea on top, an icon bar in normal flow beneath it, so typed
   text wraps full width and can never slide behind a control */
.aec-inputwrap {
  position: relative; flex: 1 1 auto;
  display: flex; flex-direction: column;
  /* FULL WIDTH, one line, no box (Paul 260730 — desktop and mobile):
     the curved top corners and the side borders are gone. What's left
     is a single horizontal divider between the conversation and where
     you type, which is all the separation it ever needed. Replaces the
     260722m bottom-sheet shape and the 260723 #5 side margins. */
  margin: 0;
  border: 0; border-top: .5px solid var(--aec-input-line);
  border-radius: 0;
  /* slight transparency, HEAVY blur behind (260722o) — what's under
     the box is soft wash, never readable shapes. glass-head, not
     glass: at .8 the dark tint over dark content read as ZERO
     transparency on Paul's phone (260722p) — the airier tint lets
     the ghosting actually show while the 26px blur keeps text clear */
  background: var(--aec-glass-input);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  backdrop-filter: blur(26px) saturate(1.5);
  overflow: hidden;
  transition: border-color .12s ease, box-shadow .12s ease;
}
/* no backdrop support: keep SOME translucency (a solid fallback read
   as "zero transparency" on Paul's phone) without unblurred text
   showing through the typing area */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .aec-inputwrap { background: var(--aec-glass); }
}
/* theme changes cross-fade instead of snapping (260722 #12) */
.aec-window, .aec-composer, .aec-drawer {
  transition: background-color .25s ease, border-color .25s ease,
              color .25s ease;
}
.aec-inputwrap .aec-input { width: 100%; }
/* the focus ring belongs to the box, not the inner text region */
#aiellie-root .aec-inputwrap .aec-input:focus-visible { outline: none; }
/* focus is a firming of the one edge that's left, not a coloured ring
   (§1). With the box gone (260730) that is the divider itself. */
.aec-inputwrap:focus-within {
  border-top-color: var(--aec-charcoal-soft);
}
/* icon bar (§8, rebuilt): [Attach, Camera] … [Speak, Send]. The
   microphone leaves the centre and joins the send end. The Hide-chat
   pill has moved into the Menu (see the JS): the composer holds
   attachment controls, voice and send, and nothing else. */
.aec-inputbar {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 10px 10px;
  position: relative;      /* ▾ Hide chat is centred against this */
}
.aec-inputbar-side { display: flex; align-items: center; gap: 8px; flex: 1 1 0; }
.aec-inputbar-end { justify-content: flex-end; gap: 6px; }
/* utility icons: quiet, 44px targets */
.aec-iconact {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--aec-charcoal-soft);
  transition: background-color .12s ease, border-color .12s ease,
              transform .09s ease;
}
.aec-iconact:hover { background: var(--aec-ivory-2); }
.aec-iconact svg { width: 21px; height: 21px; }
/* Paperclip and Camera are two SEPARATE one-tap controls (§8). The
   camera earns its own button because so much of what customers need
   help with is a photo taken on the spot — a fault, a parcel, a
   delivery label, a colour against an outfit — and burying it one tap
   deep inside a file picker costs exactly the people who need it.
   Deliberately NOT a gallery button: the paperclip already covers
   existing photos, screenshots, PDFs and files.

   MOBILE ONLY (Paul 260731). On a desktop browser `capture` is ignored,
   so the camera button opened a file picker — the paperclip's job, done
   twice — and almost nobody photographs anything with a laptop webcam.
   The button is hidden below, not deleted: the input still exists so the
   control returns intact on a phone-width viewport. */
.aec-attach, .aec-camera {
  border: 1px solid var(--aec-line); border-radius: 14px;
}
.aec-attach:hover, .aec-camera:hover { border-color: var(--aec-accent-line); }
.aec-camera { display: none; }
@media (max-width: 640px) {
  .aec-camera { display: inline-flex; }
}
/* Hide = labelled "▾ Hide chat" pill, mobile only (260722d #1 — a bare
   chevron is never self-explanatory; Paul 260729 put it back here after
   the refinement round briefly moved it into the Menu). Warm neutrals
   now, and quiet: it sits beside the two attachment controls without
   competing with them. */
.aec-minbtn {
  /* on BOTH rigs since 260809d — it is the control the chat shrinks
     back into, and the desktop header's duplicate "–" is gone with it
     (Paul 260723i: one hide control only) */
  display: inline-flex; align-items: center; gap: 5px;
  position: absolute; inset: 0 0 10px; margin: auto;
  width: -moz-fit-content; width: fit-content; height: 36px;
  height: 36px; padding: 0 12px; border: 0; border-radius: 18px;
  background: var(--aec-ivory-2); color: var(--aec-charcoal-soft);
  font-size: 12.5px; font-weight: 600; white-space: nowrap;
  transition: background-color .12s ease, transform .09s ease;
}
.aec-minbtn svg { width: 14px; height: 14px; }
@media (max-width: 640px) {
  /* Hide chat sits on the bottom CENTRE line (260809), directly over
     the launcher the panel shrinks back into. The two controls are the
     same journey in opposite directions, so they share an axis: you
     put the chat away where you picked it up. Absolutely positioned
     rather than reordered so the attachment controls and the send end
     keep the positions customers already know. */
  /* (the pill is centred at every width now — see .aec-minbtn) */
}
/* 360px Androids: three controls plus the send end is the tightest the
   bar ever gets, so the left zone tightens rather than wraps */
@media (max-width: 380px) {
  .aec-inputbar { gap: 6px; padding: 4px 8px 10px; }
  .aec-inputbar-side { gap: 5px; }
  .aec-minbtn { padding: 0 10px; font-size: 12px; }
}
/* the microphone stays available but visually recedes (260722d #2) —
   quiet in COLOUR, not in size: at 19px it read as an afterthought, so
   it goes up 50% to 28.5px (Paul 260731) inside the same 44px target. */
.aec-mic { color: var(--aec-time); }
.aec-mic svg { width: 28.5px; height: 28.5px; }
/* (the hairline between "say it" and "send it" is REMOVED — Paul
   260730. Same reason as the header one: a floating vertical rule. The
   element is gone from the JS, not hidden.) */
/* listening = a calm breath, never flashing (260722e #8) */
.aec-mic.aec-mic-on {
  background: var(--aec-sage-bg); color: var(--aec-sage-text);
  animation: aec-mic-pulse 1.8s ease-in-out infinite;
}
@keyframes aec-mic-pulse {
  0% { box-shadow: 0 0 0 0 var(--aec-sage-ring); }
  70% { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.aec-input {
  flex: 1 1 auto; resize: none;
  /* §7 — the composer is generous: it opens at TWO full lines and only
     then starts growing. Expressed in em (+ the vertical padding) so it
     stays two lines at every Text-size step, not just the default. */
  min-height: calc(2.9em + 21px); max-height: 190px;
  /* a new line of typing opens the box smoothly instead of snapping it
     24px taller in one frame (Paul 260727). A SYMMETRIC curve, not the
     reveal's ease-out: over one discrete 24px step an ease-out spends
     more than half the distance in its first frame (measured +14.4px of
     24), which reads as a snap followed by a crawl — the same finding
     that set the status fold's curve. */
  transition: height .18s cubic-bezier(.4,0,.2,1);
  padding: 15px 17px 6px;
  font-size: calc(16.5px * var(--aec-font-scale, 1)); line-height: 1.45;
  font-family: var(--aec-font);
  border: 0; background: transparent; color: var(--aec-charcoal);
}
/* the native placeholder is transparent — the rippling overlay below
   IS the visible placeholder (260722t); the attribute stays for AT */
.aec-input::placeholder { color: transparent; }
/* per-letter overlay: a light, slow, gentle wave — barely-there lift
   and brighten travelling through the words */
.aec-placeholder {
  position: absolute; top: 15px; left: 17px; z-index: 0;
  pointer-events: none; white-space: nowrap;
  color: var(--aec-placeholder);
  font-size: calc(16.5px * var(--aec-font-scale, 1)); line-height: 1.45;
  font-family: var(--aec-font);
}
.aec-placeholder i {
  font-style: normal; display: inline-block; white-space: pre;
  animation: aec-ripple 4.2s ease-in-out infinite;
}
.aec-placeholder-line { display: block; white-space: nowrap; }
/* 15% smaller again (Paul, 260820d): 80% → 68% of the composer's own
   size. With the brackets gone the size is the punctuation. */
.aec-placeholder-line-sub { font-size: 68%; letter-spacing: 0; }
/* THE hide rule (260722v): CSS tracks the real value, so text set by
   ANY path — typing, dictation, prefills — hides the overlay; the JS
   hidden-toggle is only a belt. (:placeholder-shown needs the
   transparent placeholder attribute to stay present.) */
.aec-inputwrap:has(.aec-input:not(:placeholder-shown)) .aec-placeholder {
  visibility: hidden;
}
/* amplitude halved (Paul 260722u: more subtle), and the floor raised
   from .78 to .92 (Paul 260813): the wave's own trough was what made
   the invitation read as a disabled field rather than as a prompt. */
@keyframes aec-ripple {
  0%, 24%, 100% { opacity: .92; transform: none; }
  8% { opacity: 1; transform: translateY(-.75px); }
}
/* Send = the ONE primary action: a solid circle in the primary — black
   on light, cream on dark. §12: it rests, it is not elevated. */
.aec-send {
  flex: 0 0 auto; width: 48px; height: 48px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform .1s ease, background-color .12s ease;
  background: var(--aec-send-bg); color: var(--aec-send-fg);
  border: 0;
  border-radius: 50%;
  box-shadow: none;
}
.aec-send:hover:not(:disabled) { background: var(--aec-send-hover); }
.aec-send:active:not(:disabled) { transform: scale(.92); }
/* Idle: nothing to send yet (Paul 260814). It stays exactly where it
   is and keeps its size — the composer must not reflow the moment
   somebody types — and loses its fill, so an empty screen is not
   dominated by a black disc offering to do nothing. The mark stays
   readable: this is a control WAITING, not a control refusing. */
.aec-send.aec-send-idle {
  background: var(--aec-send-idle-bg);
  color: var(--aec-send-idle-fg);
  cursor: default;
}
.aec-send { transition: transform .1s ease, background-color .16s ease,
                        color .16s ease; }
.aec-hint {
  margin-left: auto;
  font-size: 12.5px; color: var(--aec-charcoal-soft);
  padding: 2px 4px;
}
@media (max-width: 640px) {
  .aec-hint { display: none; }  /* mobile: return = newline; tap Send */
  .aec-input { font-size: calc(17px * var(--aec-font-scale, 1)); }
  /* the box touches the true bottom — the home-bar safe area lives
     INSIDE it so the icons stay reachable */
  .aec-inputwrap { padding-bottom: env(safe-area-inset-bottom); }
  .aec-placeholder { font-size: calc(17px * var(--aec-font-scale, 1)); }
}

/* misc */
.aec-spinner {
  width: 16px; height: 16px; border-radius: 50%;
  border: 2.5px solid var(--aec-line); border-top-color: var(--aec-ink);
  animation: aec-spin .9s linear infinite; flex: 0 0 auto;
}
@keyframes aec-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .aec-spinner { animation-duration: 2.5s; }
  .aec-drawer { transition: none; }
}
.aec-visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* Administrator test conversation (260728). Deliberately loud: a test
   chat that looked like the real widget would be indistinguishable from
   a customer's, and the whole point is that it cannot be confused with
   one. */
.aiellie-testbadge {
  background: #7a4d00;
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .02em;
  text-align: center;
  padding: 5px 10px;
  border-radius: 12px 12px 0 0;
}
#aiellie-root[data-aiellie-test="1"] .aec-window {
  outline: 3px solid #7a4d00;
  outline-offset: -1px;
}

/* ── colour swatches (260806) — RETIRED 260822 ────────────────────────
   The 40px round swatch, its pill label and its "my pick" tag are
   gone: a colour choice is rendered by the SAME comparison card as a
   product (Paul 260822b, "use the same design direction we just
   established"), so `.aec-swatch*` had no elements left to style. The
   doctrine it was built on did not go with it — the pictures are still
   the thing being compared, the row is still capped at three, and the
   full-size view is still one tap away rather than always on screen.
   See .aec-products / .aec-product-card above, and renderColourChoices
   in aiellie.js. */

/* Enlarged view: inside the chat window, not a new page — the point of
   showing the colour here is that nobody has to leave the conversation
   to look at it. */
.aec-lightbox {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(26, 25, 23, .72);
}
.aec-lightbox__box {
  position: relative;
  max-width: 100%;
  max-height: 100%;
  background: var(--aec-surface);
  border-radius: 10px;
  padding: 10px;
  text-align: center;
}
.aec-lightbox__img {
  display: block;
  max-width: 100%;
  /* leaves room for the caption and the close control on a short phone */
  max-height: 60vh;
  margin: 0 auto;
  border-radius: 6px;
  /* the same gesture shrinks it again (260807) */
  cursor: zoom-out;
}
.aec-lightbox__cap {
  margin-top: 8px;
  font-size: 12px;
  color: var(--aec-charcoal-soft);
}
.aec-lightbox__close {
  position: absolute;
  top: -10px;
  right: -10px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--aec-bubble-agent-line);
  background: var(--aec-surface);
  color: var(--aec-charcoal);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

/* ── contextual tips (260806) ──────────────────────────────────────────
   The unused space beside a short customer message, used occasionally
   to teach one useful thing.

   ABSOLUTELY POSITIONED against the row on purpose: out of flow means
   it cannot change the row's height, narrow the bubble, move a message,
   or shift anything when it appears, collapses or reopens. The row
   itself only gains `position: relative`, which changes no geometry.

   Visually subordinate to everything: quieter than a message bubble,
   quieter than a product card, no shadow heavier than a bubble's, no
   pulse, no dot, nothing that reads as an unread alert. */
.aec-msg--tipped { position: relative; }
.aec-tip {
  position: absolute;
  /* 22px, not 10 (Paul 260808): "clearly separated from the chat
     bubble". At 10px the note sat against the bubble's edge and read as
     part of it — the whole point of a margin note is the margin. */
  right: calc(100% + 22px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  /* LEFT-ALIGNED IN ITS SPACE (Paul 260808). This was row-reverse,
     which packs from the right — so the text ended up hard against the
     bubble and its left edge moved with every sentence length. Normal
     row order packs from the left instead: text first at the same left
     edge every time, bulb after it, and whatever room is left over
     stays as clear space beside the bubble. */
  flex-direction: row;
  justify-content: flex-start;
  pointer-events: none;          /* only the controls take clicks */
  z-index: 1;
}
.aec-tip__bulb {
  pointer-events: auto;
  /* 16-18px of visible bulb inside a 40px target (Paul's spec) */
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border-radius: 50%;
}
.aec-tip__icon {
  font-size: 17px;
  line-height: 1;
  /* Faint, but never below the contrast a control needs: greyscale and
     a floor on opacity rather than a pale yellow that vanishes on
     ivory. Hover/focus bring it to full strength. */
  filter: grayscale(1);
  opacity: .55;
  transition: opacity .15s ease;
}
.aec-tip__bulb:hover .aec-tip__icon,
.aec-tip__bulb:focus-visible .aec-tip__icon { opacity: 1; }
.aec-tip__bulb:focus-visible {
  outline: 2px solid var(--aec-focus);
  outline-offset: -4px;
}
/* JUST THE TEXT (Paul 260806, revising his own brief: "the style should
   NOT look like another chat bubble"). No surface, no border, no
   shadow, no radius — nothing that could be mistaken for something
   Ellie said. It reads as a note in the margin, because that is what it
   is. */
.aec-tip__text {
  pointer-events: auto;
  /* The bulb is 40px and the gap 6px; without this the text is handed
     the whole measured room and then clipped by the clamp — which is
     how "Click the image to enlarge it." became "Click the image to"
     (Paul, 260806). The JS also verifies the rendered result, because
     a calculation is not a rendering. */
  max-width: calc(100% - 48px);   /* 40px bulb + the 8px gap */
  color: var(--aec-charcoal-soft);
  font-size: 12px;
  line-height: 1.35;
  /* Left, per Paul 260806. Right-aligned wrapping gave a ragged left
     edge that read as a second speaker's bubble aligned to the wrong
     side; left-aligned it reads as a margin note, which is what it is. */
  text-align: left;
  /* two short lines, never more */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  opacity: 1;
  transition: opacity .4s ease;
}
/* The brief flash — Paul: "like a bad tv signal to get attention". It
   runs ONCE, for six tenths of a second, and never again: the class is
   removed after it, so reopening the tip is quiet. This is deliberately
   the one exception to "nothing that demands attention" — a note nobody
   notices is not a note. */
@keyframes aec-tip-signal {
  0%   { opacity: 0; }
  7%   { opacity: 1; }
  13%  { opacity: .12; }
  21%  { opacity: .95; transform: translateX(-1px); }
  29%  { opacity: .25; }
  37%  { opacity: 1; transform: translateX(1px); }
  45%  { opacity: .5; }
  60%  { opacity: 1; transform: none; }
  72%  { opacity: .75; }
  100% { opacity: 1; }
}
.aec-tip--signal .aec-tip__text {
  animation: aec-tip-signal .62s steps(1, end) 1 both;
}
/* Collapsed: the text goes, the bulb stays exactly where it was — no
   reflow, because it was never in flow. */
.aec-tip[data-open="0"] .aec-tip__text {
  opacity: 0;
  pointer-events: none;
}
/* Reduced motion: no flicker at all, and no fade — the text simply is
   there, and simply goes. */
.aec-tip--still .aec-tip__text,
.aec-tip--still .aec-tip__icon { transition: none; }
.aec-tip--still .aec-tip__text { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .aec-tip__text, .aec-tip__icon { transition: none; }
  .aec-tip--signal .aec-tip__text { animation: none; }
}
/* Hover anywhere on the tip brings the text back on a pointer device;
   the bulb's focus handler does the same from the keyboard. */
@media (hover: hover) {
  .aec-tip:hover .aec-tip__text { opacity: 1; pointer-events: auto; }
}

/* ── theme change: a cross-fade, not a flash (Paul 260813) ───────────
   Every colour in the widget is a token, so switching Appearance
   repaints the whole panel in one frame — correct, and abrupt. The
   transition is added for 200ms AROUND the change rather than living
   on the elements permanently: a standing transition on every colour
   would also animate hovers, receipts and the reveal, which must stay
   instant. Reduced motion opts out below. */
#aiellie-root.aec-theming,
#aiellie-root.aec-theming * {
  transition: background-color .18s ease, color .18s ease,
              border-color .18s ease, box-shadow .18s ease,
              fill .18s ease, stroke .18s ease !important;
}

/* ── the open menu sits on something (Paul 260813) ───────────────────
   A hair of blur and the faintest wash, INSIDE the window only. Not a
   modal scrim: the conversation must stay readable and must not look
   disabled — what it stops is the menu's glass borrowing the text
   directly behind it. Tapping it closes the menu, which the outside
   click handler already does. */
.aec-menu-scrim {
  position: absolute; inset: 0; z-index: 7;
  /* It must not SWALLOW anything. A wash that intercepts clicks is a
     modal by another name: the header controls stop working, and "does
     not look disabled" is not the same promise as "is not disabled".
     Closing is already handled by the outside-click listener, which
     runs on mousedown wherever the tap lands. */
  pointer-events: none;
  background: var(--aec-scrim);
  -webkit-backdrop-filter: blur(1px);
  backdrop-filter: blur(1px);
  animation: aec-scrim-in .17s ease both;
}
@keyframes aec-scrim-in { from { opacity: 0; } to { opacity: 1; } }
.aec-menu-scrim.aec-menu-out { animation: aec-scrim-out .15s ease both; }
@keyframes aec-scrim-out { from { opacity: 1; } to { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  #aiellie-root.aec-theming, #aiellie-root.aec-theming * {
    transition: none !important;
  }
  .aec-menu, .aec-menu.aec-menu-out,
  .aec-menu-scrim, .aec-menu-scrim.aec-menu-out { animation: none; }
}

/* A photo the customer sent, shown as a photo (Paul, 260815).
   It was a paperclip and a filename — and for a pasted screenshot that
   filename was a timestamp longer than the bubble. Capped so a portrait
   phone photo cannot push the conversation off the screen. */
.aec-msg-photo {
  display: flex; flex-direction: column; gap: 4px;
  align-items: flex-end; padding: 4px 4px 2px;
}
.aec-from-agent .aec-msg-photo { align-items: flex-start; }
.aec-msg-photo img {
  /* Fixed caps, NOT a percentage: the box shrink-wraps its content, so
     a % resolved against it collapsed a portrait photo to 87px wide
     (measured, 260815). And no object-fit — `cover` CROPS, which is
     precisely wrong for a picture of a moth hole. max-width + max-height
     with auto sizing keeps the whole frame and the aspect ratio. */
  max-width: 240px; max-height: 280px;
  width: auto; height: auto;
  border-radius: 12px; border: 1px solid var(--aec-glass-line);
  background: var(--aec-surface-2, rgba(0, 0, 0, .04));
  cursor: zoom-in;
}
/* `.aec-msg-photo-name` is gone (260820d) — a photo is its own caption
   and every filename a browser hands us is machine spelling. The rule
   went with the element rather than being left behind styling nothing. */

/* ── 260821 timings under reduced motion ─────────────────────────────
   The half-second wait and appear-only-when-finished ordering survive —
   they are SEQUENCING, not decoration — but everything arrives as a
   plain fade with no movement, and the answered quick-reply row shrinks
   without the glide. Kept at the end of the sheet: several older tests
   read the first occurrence of a selector as its defining block, so the
   grouped override must not sit above any of them. */
@media (prefers-reduced-motion: reduce) {
  /* (.aec-swatches went with the swatch, 260822 — colour choices are
     .aec-products now, and are covered by it.) */
  .aec-quick, .aec-page-actions, .aec-products,
  .aec-welcome-acts {
    animation-name: aec-fade-in;
  }
  #aiellie-root .aec-pickbtn { transition: none; }
  #aiellie-root .aec-quick--answered .aec-quick__btn,
  .aec-welcome--answered .aec-welcome-act {
    transition: none;
  }
}

/* ── replayed answered records (Paul 260821e) ────────────────────────
   A rebuilt record is not arriving — it was always there. No entrance,
   no half-second delay on the welcome acts, no shrink transition: it
   renders directly in its final shrunk state. Kept at the end of the
   sheet so these overrides sit below every rule they quiet. */
.aec-replay,
.aec-replay .aec-msg,
.aec-replay .aec-welcome-acts {
  animation: none;
}
#aiellie-root .aec-quick--answered.aec-replay .aec-quick__btn,
.aec-welcome--answered.aec-replay .aec-welcome-act {
  transition: none;
}

/* ── End-of-Chat Feedback Card & Controls ───────────────────────────── */
.aec-feedback-card {
  margin: 16px 12px;
  padding: 16px;
  background: var(--aec-surface-1, #fff);
  border: 1px solid var(--aec-border, #e5ded6);
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  /* It arrives, it doesn't appear (Paul, 260825): the JS mounts it in
     the --enter state and releases it two frames later. */
  transition: opacity 0.45s ease, transform 0.45s ease;
}
.aec-feedback-card.aec-feedback--enter {
  opacity: 0;
  transform: translateY(10px);
}
@media (prefers-reduced-motion: reduce) {
  .aec-feedback-card {
    transition: none;
  }
  .aec-feedback-card.aec-feedback--enter {
    opacity: 1;
    transform: none;
  }
}
.aec-feedback-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--aec-text, #2c2523);
  margin-bottom: 10px;
}
/* The invitation to say more, shown once they have rated. */
.aec-feedback-prompt {
  font-size: 12px;
  color: var(--aec-muted, #6f625c);
  margin-bottom: 6px;
}
.aec-feedback-stars {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
}
.aec-star-btn {
  background: none;
  border: none;
  font-size: 22px;
  color: #d1c7bd;
  cursor: pointer;
  padding: 2px 4px;
  line-height: 1;
  transition: color 0.15s ease, transform 0.15s ease;
}
/* CUMULATIVE, not pick-one (Paul, 260824). `:hover` lit the single star
   under the cursor, so four stars read as "the fourth star" rather than
   "four stars" — and the fill only became cumulative after the click,
   by which time the rating had already been sent. The preview is driven
   from JS (`aec-star--preview`) so it can light 1..N; the bare :hover
   rule that contradicted it is gone. */
.aec-star-btn.aec-star--preview,
.aec-star-btn.aec-star--active {
  color: #e5a93b;
}
.aec-star-btn.aec-star--preview {
  transform: scale(1.1);
}
.aec-feedback-comment {
  margin-bottom: 12px;
}
.aec-feedback-textarea {
  width: 100%;
  box-sizing: border-box;
  font-family: inherit;
  font-size: 13px;
  padding: 8px 10px;
  border: 1px solid var(--aec-border, #d9d2c9);
  border-radius: 6px;
  resize: vertical;
  min-height: 54px;
}
.aec-feedback-submit {
  display: block;
  margin-top: 6px;
  font-size: 12px;
  font-weight: 500;
  padding: 5px 12px;
  background: var(--aec-brand, #4a1525);
  color: #fff;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}
.aec-feedback-thanks {
  font-size: 12px;
  color: #2b7a4b;
  font-weight: 500;
  margin-top: 6px;
}
.aec-feedback-transcript {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--aec-border-subtle, #f0ebe4);
  font-size: 12px;
  color: var(--aec-text-subtle, #786c66);
}
.aec-transcript-link,
.aec-transcript-btn {
  color: var(--aec-brand, #4a1525);
  text-decoration: underline;
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  font-size: inherit;
  font-family: inherit;
}
.aec-transcript-btn[disabled] {
  color: #999;
  text-decoration: none;
  cursor: default;
}
.aec-transcript-sep {
  color: #b5aba2;
  margin: 0 4px;
}
