/* =============================================================
   ENGNOVA — Component primitives  ·  V2 "Neon Space Premium"
   Quiet 90% / Loud 10%:
   - Default = hairline borders, flat, generous radius.
   - The STICKER treatment (black outline + hard offset shadow)
     is a LOUD BUDGET — spend on ONE element per screen
     (primary CTA / flagship card / celebration moment).
   - Dark space surfaces use GLOW, never hard black shadows.
   ============================================================= */

/* ---------- BUTTONS ---------- */
.eng-btn {
  --btn-stroke: var(--eng-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-body);
  line-height: 1;
  padding: 13px 24px;
  min-height: 46px;
  border: 2px solid var(--btn-stroke);
  border-radius: var(--radius-pill);
  background: var(--eng-green);
  color: #07130B;
  box-shadow: 3px 3px 0 0 var(--btn-stroke);
  cursor: pointer;
  text-decoration: none;
  transition:
    transform var(--dur-fast) var(--ease-spring),
    box-shadow var(--dur-fast) var(--ease-spring),
    background var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out);
  user-select: none;
  white-space: nowrap;
}
.eng-btn:hover  { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 0 var(--btn-stroke); }
.eng-btn:active { transform: translate(2px, 2px);  box-shadow: 1px 1px 0 0 var(--btn-stroke); }
.eng-btn:focus-visible { outline: 3px solid var(--eng-purple); outline-offset: 3px; }

/* Sizes */
.eng-btn--sm { font-size: var(--fs-body-sm); padding: 9px 18px; min-height: 36px; box-shadow: 2px 2px 0 0 var(--btn-stroke); }
.eng-btn--sm:hover { box-shadow: 3px 3px 0 0 var(--btn-stroke); }
.eng-btn--lg { font-size: var(--fs-body-lg); padding: 17px 32px; min-height: 58px; box-shadow: 4px 4px 0 0 var(--btn-stroke); }
.eng-btn--lg:hover { box-shadow: 5px 5px 0 0 var(--btn-stroke); }

/* Variants */
.eng-btn--primary { background: var(--eng-green);  color: #07130B; }
.eng-btn--purple  { background: var(--eng-purple); color: var(--eng-white); }
.eng-btn--yellow  { background: var(--eng-yellow); color: var(--eng-ink); }
.eng-btn--black   { background: var(--eng-ink);    color: var(--eng-white); }

/* Quiet secondary — outline only, no shadow (inverts on hover) */
.eng-btn--outline { background: transparent; color: var(--eng-ink); box-shadow: none; }
.eng-btn--outline:hover  { background: var(--eng-ink); color: var(--eng-white); transform: none; box-shadow: none; }
.eng-btn--outline:active { transform: translate(1px, 1px); box-shadow: none; }

/* Dark-surface CTA — glow instead of hard shadow */
.eng-btn--glow { box-shadow: 0 0 28px rgba(57, 255, 137, 0.35); }
.eng-btn--glow:hover  { transform: translateY(-2px); box-shadow: 0 0 44px rgba(57, 255, 137, 0.55); }
.eng-btn--glow:active { transform: translateY(0);    box-shadow: 0 0 20px rgba(57, 255, 137, 0.35); }

/* Ghost — text only */
.eng-btn--ghost {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--eng-ink);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1.5px;
}
.eng-btn--ghost:hover  { transform: none; box-shadow: none; color: var(--eng-purple); }
.eng-btn--ghost:active { transform: none; box-shadow: none; }

/* Dark-surface AA overrides (audit:contrast, 2026-07-11) — ghost/outline
   buttons default to ink text, which is invisible on the graphite admin
   chrome and the purple .eng-bg-grid hero (1.2-3.3:1 measured). Mirrors the
   .eng-bg-space overrides in learner.css, which admin/placement surfaces do
   not always load — these live here in the always-loaded token sheet. */
.bg-eng-graphite .eng-btn--ghost, .eng-bg-grid .eng-btn--ghost { color: rgba(255, 255, 255, 0.85); }
.bg-eng-graphite .eng-btn--ghost:hover, .eng-bg-grid .eng-btn--ghost:hover { color: var(--eng-white); }
.bg-eng-graphite .eng-btn--outline, .eng-bg-grid .eng-btn--outline { color: var(--eng-white); border-color: rgba(255, 255, 255, 0.45); }
.bg-eng-graphite .eng-btn--outline:hover, .eng-bg-grid .eng-btn--outline:hover { background: var(--eng-white); color: var(--eng-ink); }

.eng-btn:disabled {
  background: var(--eng-fog);
  color: var(--eng-slate);
  border-color: var(--eng-silver);
  box-shadow: none;
  cursor: not-allowed;
  pointer-events: none;
}

/* Icon-only round button — quiet */
.eng-icon-btn {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--hairline);
  background: var(--eng-white);
  color: var(--eng-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.eng-icon-btn:hover  { border-color: var(--eng-ink); }
.eng-icon-btn:active { background: var(--eng-paper); }

/* ---------- CARDS ---------- */
/* Default = quiet: hairline, flat. */
.eng-card {
  background: var(--eng-white);
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
}
.eng-card--lg { border-radius: var(--radius-xl); padding: var(--space-8); }

/* LOUD variant — the sticker. One per screen. */
.eng-card--sticker { border: 2px solid var(--eng-ink); box-shadow: 5px 5px 0 0 var(--eng-ink); }

/* Colored surfaces keep a black contour (they're brand moments) */
.eng-card--purple { background: var(--eng-purple); color: var(--eng-white); border: 2px solid var(--eng-ink); }
.eng-card--green  { background: var(--eng-green);  color: var(--eng-ink);   border: 2px solid var(--eng-ink); }
.eng-card--yellow { background: var(--eng-yellow); color: var(--eng-ink);   border: 2px solid var(--eng-ink); }
.eng-card--dark   { background: var(--eng-space);  color: var(--eng-white); border: 1px solid var(--hairline-dark); }

/* Pill / tag — quiet outline, no shadow */
.eng-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-body-en);
  font-weight: 700;
  font-size: var(--fs-caption);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  padding: 5px 12px;
  border: 1.5px solid var(--eng-ink);
  border-radius: var(--radius-pill);
  background: var(--eng-yellow);
  color: var(--eng-ink);
  white-space: nowrap;
}
.eng-tag--green  { background: var(--eng-green); }
.eng-tag--purple { background: var(--eng-purple); color: var(--eng-white); border-color: var(--eng-purple); }
.eng-tag--white  { background: var(--eng-white); border-color: var(--hairline); }
.eng-tag--black  { background: var(--eng-ink); color: var(--eng-white); }

/* ---------- INPUTS ---------- */
.eng-input,
.eng-textarea,
.eng-select {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--font-body);
  font-size: var(--fs-body);
  color: var(--eng-ink);
  background: var(--eng-white);
  border: 1.5px solid #D9D9D4;
  border-radius: var(--radius-md);
  padding: 13px 16px;
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
  outline: none;
}
.eng-input:hover, .eng-textarea:hover, .eng-select:hover { border-color: var(--eng-ink); }
.eng-input:focus, .eng-textarea:focus, .eng-select:focus {
  border-color: var(--eng-purple);
  box-shadow: 0 0 0 3px rgba(99, 53, 240, 0.20);
}
.eng-input::placeholder { color: var(--eng-slate); }

.eng-label {
  display: block;
  font-family: var(--font-body-en);
  font-weight: 700;
  font-size: var(--fs-body-sm);
  margin-bottom: 6px;
  color: var(--eng-ink);
}

/* ---------- XP / PROGRESS ---------- */
.eng-progress {
  position: relative;
  height: 16px;
  background: #EFEDF6;
  border: 1.5px solid #D9D9D4;
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.eng-progress__fill {
  height: 100%;
  background: var(--eng-green);
  transition: width var(--dur-slow) var(--ease-spring);
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(0,0,0,0) 0 8px,
    rgba(0,0,0,0.08) 8px 12px
  );
}
.eng-progress--lg { height: 24px; }

/* Dark-surface progress */
.eng-progress--dark { background: rgba(255,255,255,0.10); border-color: var(--hairline-dark); }
.eng-progress--dark .eng-progress__fill { box-shadow: 0 0 16px rgba(57,255,137,0.45); }

/* ---------- DIVIDERS ---------- */
.eng-divider { height: 1.5px; background: var(--hairline); border: none; margin: var(--space-6) 0; }
.eng-divider--dashed { border-top: 2px dashed var(--eng-silver); background: transparent; height: 0; }

/* ---------- MOTION ---------- */
@keyframes eng-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(4px); }
}
.eng-shake { animation: eng-shake 360ms var(--ease-in-out); }

@keyframes eng-pop {
  0% { transform: scale(0.6); opacity: 0; }
  60% { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); }
}
.eng-pop { animation: eng-pop 360ms var(--ease-spring); }

@keyframes eng-float {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-6px) rotate(-3deg); }
}
.eng-float { animation: eng-float 4s ease-in-out infinite; }

@keyframes eng-glow-pulse {
  0%, 100% { opacity: 0.75; }
  50%      { opacity: 1; }
}
.eng-glow-pulse { animation: eng-glow-pulse 3s ease-in-out infinite; }

@keyframes eng-spin-slow { to { transform: rotate(360deg); } }
.eng-spin-slow { animation: eng-spin-slow 24s linear infinite; }

/* ---------- AMBIENT LIBRARY (2026-09-01) ----------
   Six primitives so a page can be alive at idle instead of only on interaction.
   Grammar adapted from goldradar.in.th (see docs/design-reference/
   MOTION-LANGUAGE.md); every colour here is an Engnova token — the source
   site's gold/cyan palette is NOT imported and the V2 design freeze stands.

   What was already here and is NOT duplicated: --ease-out is the same glide
   curve, --ease-spring the same overshoot, eng-float the same drift,
   eng-pop the same stamp, eng-glow-pulse the same soft breath.

   All six are OPT-IN classes. Adding them to this shared sheet animates
   nothing until a class is applied, so admin and learner surfaces are
   untouched until they ask. Each is listed in the reduced-motion block below.
   Set animation-delay per instance — primitives in lockstep read as one
   mechanism twitching, which is the opposite of ambient. */

/* A dot that blinks like an instrument light. Stagger 200-600ms apart. */
@keyframes eng-blip {
  0%, 100% { opacity: 0;   transform: scale(0.5); }
  20%      { opacity: 1;   transform: scale(1); }
  60%      { opacity: 0.4; transform: scale(0.8); }
}
.eng-blip { animation: eng-blip 3.4s ease-in-out infinite; }

/* A ring that swells around a control — for the ONE loud CTA on a screen.
   The colour is a KNOB, not a constant: --glow-accent, defaulting to green
   because that is what this codebase already does on its own CTA
   (landing.css green glow at rest 0.30 → hover 0.5) and on the flagship
   drop zones. A first draft hardcoded purple with a comment claiming a green
   halo on a green button says nothing; the comment was invented and the two
   existing call sites say the opposite.

   A knob rather than a fixed hue is what makes "one accent per section" a
   mechanism instead of a discipline: a section sets --glow-accent once on its
   container and everything inside glows in its own colour. */
@keyframes eng-breathe {
  0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--glow-accent) 34%, transparent),
                         0 0 26px color-mix(in srgb, var(--glow-accent) 32%, transparent); }
  50%      { box-shadow: 0 0 0 1px color-mix(in srgb, var(--glow-accent) 62%, transparent),
                         0 0 46px color-mix(in srgb, var(--glow-accent) 52%, transparent); }
}
.eng-breathe { animation: eng-breathe 3.2s ease-in-out infinite; }

/* Light travelling across a gradient. Requires background-size: 200% on the
   element — without it the position has nowhere to move and this is inert. */
@keyframes eng-shimmer {
  0%, 100% { background-position: 0% 50%; }
  50%      { background-position: 100% 50%; }
}
.eng-shimmer { background-size: 200% auto; animation: eng-shimmer 3.6s ease-in-out infinite; }

/* A backdrop grid sliding by one cell. Pair with a 60px background-size. */
@keyframes eng-drift {
  from { background-position: 0 0; }
  to   { background-position: 60px 60px; }
}
.eng-drift { animation: eng-drift 22s linear infinite; }

/* A number or badge that asks to be looked at once in a while. */
@keyframes eng-ring-pulse {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 8px color-mix(in srgb, var(--glow-accent) 42%, transparent); }
  50%      { transform: scale(1.09); box-shadow: 0 0 20px color-mix(in srgb, var(--glow-accent) 72%, transparent); }
}
.eng-ring-pulse { animation: eng-ring-pulse 2.8s ease-in-out infinite; }

/* Entrance. Longer than an interaction on purpose: --dur-enter is the arrival
   band, --dur-base is the response band, and mixing them makes a page that
   answers a click as slowly as it introduces itself. */
@keyframes eng-rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
.eng-rise { animation: eng-rise var(--dur-enter) var(--ease-out) both; }

/* ---------- HOVER GRAMMAR ----------
   One rule for everything that reacts: lift a little, glow a little, 200ms.
   Named properties, never `all` — `all` animates whatever a future rule adds,
   including layout properties, and then the lift stutters for reasons nobody
   can find. */
.eng-lift {
  transition: transform var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
.eng-lift:hover,
.eng-lift:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 0 24px color-mix(in srgb, var(--glow-accent) 30%, transparent);
}
/* Glow only — no black offset shadow. The V2 law is "glow, never black offset
   shadows, on dark", and on --eng-space a black drop is invisible anyway: it
   would be dead declaration on the surfaces this is mostly used on, and the
   wrong idiom on the rest. Light surfaces get their depth from .eng-card. */
/* A finger cannot hover. Without this the lift fires on tap and stays stuck
   until you touch something else, which reads as a bug. */
@media (hover: none) and (pointer: coarse) {
  .eng-lift:hover { transform: none; box-shadow: none; }
}

/* =================================================================
   ENGNOVA LOCAL EXTENSIONS — on top of the FROZEN handoff-v2 base.
   Base above is byte-identical to docs/design-reference/handoff-v2/
   components.css (design freeze 2026-07-04). Blocks below survive
   reconciliation per DECISIONS 2026-07-04 rulings E1-E4.
   ================================================================= */

/* ---- DD-5 variants, re-added V2-CONFORMANT (ruling E4) ----------
   Originally referenced-but-undefined (Cluster DD triage). Redefined
   in the V2 quiet language: no sticker shadows on quiet variants;
   red is a TINT, never a full field (V2 rule 5). */
.eng-card--sm  { border-radius: var(--radius-md); padding: var(--space-4); }
.eng-tag--yellow { background: var(--eng-yellow); }  /* explicit (matches base) */
.eng-tag--red  {
  background: var(--eng-wrong-tint);
  color: var(--eng-wrong-text);
  border-color: var(--eng-red);
}
.eng-tag--ghost { background: transparent; }  /* outline-only quiet tag */
/* sales_manager badge (Migration 064) — Premium-gold field at micro-accent
   size, default dark ink (the eng-tag--yellow pattern; gold-edge TEXT on the
   light admin surface measures ~2.7:1 and fails AA, so ink stays dark) */
.eng-tag--gold { background: var(--eng-gold); border-color: var(--eng-gold-edge); }

/* ----------------------------------------------------------------
   FORM ERROR CHIP — wrong-state as tint (V2 rule 5), soft copy.
   MOVED here from learner.css (CL-LEADS 2026-07-17): admin surfaces
   (leads / payments / magic-links / lead-detail) use the chip but don't
   load the learner sheet — it rendered unstyled there. Token-sheet home =
   every page gets it. The [hidden] guard keeps state-toggling JS working
   (display:block would otherwise defeat the UA [hidden] rule — the LC-1
   error-chip leak class). */
.lrn-error-chip {
  display: block;
  background: var(--eng-wrong-tint);
  color: var(--eng-wrong-text);
  border: 1.5px solid var(--eng-red);
  border-radius: 12px;
  padding: 10px 14px;
  font-size: var(--fs-body-sm);
  overflow-wrap: anywhere;
}
.lrn-error-chip[hidden] { display: none; }

/* Success twin of the error chip — the (71) fix: an admin action that
   succeeded must SAY so on the reloaded screen, because a reload that looks
   like the same page is exactly how a silent failure and a silent success
   became indistinguishable. Same token-sheet home as the error chip, same
   [hidden] guard. Border is --eng-green-deep, not neon --eng-green: the neon
   sits at ~1.2:1 against the tint and vanishes; deep is the brand's stated
   green edge. */
.lrn-done-chip {
  display: block;
  background: var(--eng-correct-tint);
  color: var(--eng-correct-text);
  border: 1.5px solid var(--eng-green-deep);
  border-radius: 12px;
  padding: 10px 14px;
  font-size: var(--fs-body-sm);
  overflow-wrap: anywhere;
}
.lrn-done-chip a { color: inherit; font-weight: 700; }
.lrn-done-chip[hidden] { display: none; }

/* ----------------------------------------------------------------
   Q-Q4 MOOD PRESET DURATION SCALING (Cluster Q infrastructure-only).
   Scales Saturn motion duration tokens per <body data-mood="..."> attribute
   (3 functional moods per Q-Q-LEAN-1 lock). Saturn easing curves
   (--ease-spring/--ease-out/--ease-in-out) preserved as canonical authority;
   only duration tokens scale per mood (Q-Q-LEAN-3 1.0×/1.6×/0.7×).
   Cascade: :root defaults → [data-mood="..."] override at body scope →
   inheritance to descendants → existing transition/animation rules consume
   var(--dur-*) at scaled value.
   ---------------------------------------------------------------- */
[data-mood="calm"] {
  --dur-fast: 200ms;   /* 120ms × 1.6× */
  --dur-base: 320ms;   /* 200ms × 1.6× */
  --dur-slow: 580ms;   /* 360ms × 1.6× */
  --dur-enter: 1120ms; /* 700ms × 1.6× */
}
[data-mood="focused"] {
  --dur-fast: 80ms;    /* 120ms × 0.7× */
  --dur-base: 140ms;   /* 200ms × 0.7× */
  --dur-slow: 250ms;   /* 360ms × 0.7× */
  --dur-enter: 490ms;  /* 700ms × 0.7× */
}
/* energetic = Saturn :root defaults (no override needed) */

/* ----------------------------------------------------------------
   Q-Q6 ACCESSIBILITY: prefers-reduced-motion baseline (WCAG 2.1 SC 2.3.3).
   Disables Saturn motion classes + transitions when user prefers reduced
   motion. Targeted approach (vs universal `*` !important) preserves
   semantic motion elsewhere; transition-duration: 0.01ms (NOT 0) preserves
   transitionend events for any JS relying on them. Declared LAST in file
   (after :root + [data-mood] overrides) to win source-order tie at
   media-active state per W3C MediaQueries-5 cascade. V2 additions:
   .eng-glow-pulse joins the disabled-animation list (design freeze).
   2026-08-22: .adm-bar__fill joins the transition list. The admin console
   has no reduced-motion block of its own and does not get one — a second
   block is a second place to keep in sync, and this is the canonical one.
   The bar still ARRIVES at its width; only the 200ms draw-in is removed.
   ---------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .eng-shake,
  .eng-pop,
  .eng-float,
  .eng-spin-slow,
  .eng-glow-pulse,
  .eng-blip,
  .eng-breathe,
  .eng-shimmer,
  .eng-drift,
  .eng-ring-pulse,
  .eng-rise {
    animation-name: none;
  }
  /* eng-rise is an ENTRANCE: killing the animation must leave the element at
     its destination, not at translateY(18px). `both` fill would strand it. */
  .eng-rise { opacity: 1; transform: none; }
  .eng-lift { transition-duration: 0.01ms; }
  .eng-lift:hover, .eng-lift:focus-visible { transform: none; }
  .eng-btn,
  .eng-icon-btn,
  .eng-input,
  .eng-textarea,
  .eng-select,
  .eng-progress__fill,
  .adm-bar__fill {
    transition-duration: 0.01ms;
  }
}

/* ── SW-3: coach thread bubbles (light admin/coach surfaces) ─────────────
   Learner-side bubbles live in learner.css (.lrn-msg); this is the staff-
   side mirror on the light workspace. Tokens only. */
.coach-msg { display: flex; flex-direction: column; align-items: flex-start; max-width: 85%; }
.coach-msg--mine { align-self: flex-end; align-items: flex-end; }
.coach-msg__bubble {
  background: var(--eng-paper);
  border: 1.5px solid var(--hairline);
  border-radius: 14px 14px 14px 4px;
  padding: 8px 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.coach-msg--mine .coach-msg__bubble {
  background: var(--eng-purple-soft);
  border-color: color-mix(in srgb, var(--eng-purple) 35%, transparent);
  border-radius: 14px 14px 4px 14px;
}
.coach-msg__meta { margin-top: 2px; font-size: 0.72rem; color: var(--eng-slate); }
.coach-note__body { white-space: pre-wrap; overflow-wrap: anywhere; }

/* P0-B B5: destructive action button — red as TINT (V2 rule 5), visually
   distinct from the invite/primary actions. */
.eng-btn--danger {
  background: var(--eng-wrong-tint);
  color: var(--eng-wrong-text);
  border-color: var(--eng-red);
}
.eng-btn--danger:hover { background: var(--eng-wrong-tint); filter: brightness(0.96); }

/* =================================================================
   Consent banner (M1-B-1) — 2026-07-27.
   GLOBAL (here, not learner.css) because it mounts on the standalone landing
   too — landing loads only landing.css, so a learner.css home left the banner
   unstyled (black text). Fixed bottom bar, V2 dark (--eng-space) + a
   white-channel hairline top (dark-surface hairline convention — zero new hex).
   z-index 45 sits BELOW the celebration tier (mascot z-55 / overlay z-80).
   Sets no `display` → the [hidden] attr's UA rule wins with no guard needed;
   consent-banner.js reveals it only when there's no engnova.consent choice.
   ================================================================= */
.cns-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  background: var(--eng-space);
  border-top: 1.5px solid color-mix(in srgb, var(--eng-white) 12%, transparent);
  padding: 12px 16px;
  box-shadow: 0 -8px 24px color-mix(in srgb, var(--eng-space) 60%, transparent);
}
.cns-banner__inner {
  max-width: min(72rem, 100%);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 20px;
}
.cns-banner__text {
  flex: 1 1 320px;
  margin: 0;
  color: var(--eng-white);
  font-size: var(--fs-body-sm);
  line-height: 1.5;
}
.cns-banner__text a {
  color: var(--eng-green);
  text-decoration: underline;
  text-underline-offset: 3px;
  white-space: nowrap;
}
.cns-banner__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* Essential-only button: solid neutral (light chip on the dark bar) — equal
   size + prominence to the green accept (PDPA freely-given), just not the
   affirmative colour. Tokens only. */
.cns-banner__btn {
  background: var(--eng-white);
  color: var(--eng-ink);
  border-color: var(--eng-white);
}

/* ── LIVE CHAT widget (ทีม Ground Crew, 2026-09-04) ──────────────────────
   Global sheet on purpose: the widget mounts on every layouts/main page and
   the landing loads ONLY landing.css — the .cns-banner precedent. Dark FD
   glass panel (works on light AND dark grounds), green launcher = action
   colour. Launcher is STATIC — no pulse: a blinking light claims live state
   this control does not have (the de-dialect lens). Tokens only. */
/* --cns-h: the consent banner's height while it is showing (set by
   consent-banner.js, unset otherwise), so the launcher sits ABOVE the bar
   instead of on its buttons — PF-CHAT-1, every widget page, 390 worst. */
.cht { position: fixed; right: 18px; bottom: calc(18px + var(--cns-h, 0px)); z-index: 60; }
.cht__launcher {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 18px;
  border: 2px solid var(--eng-black);
  border-radius: var(--radius-pill);
  background: var(--eng-green);
  color: var(--eng-ink);
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
  cursor: pointer;
  box-shadow: var(--shadow-sticker-lg);
}
.cht__launcher-icon { font-size: 18px; line-height: 1; }
.cht__panel {
  display: none;
  width: min(360px, calc(100vw - 36px));
  height: min(540px, calc(100vh - 120px));
  flex-direction: column;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--radius-xl);
  background: var(--eng-space);
  color: var(--eng-white);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.cht__panel.is-open { display: flex; }
.cht__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
}
.cht__title { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--eng-white); }
.cht__sub { margin: 2px 0 0; font-size: 12px; color: rgba(255, 255, 255, 0.7); }
.cht__close {
  border: 0; background: transparent; cursor: pointer;
  color: rgba(255, 255, 255, 0.75); font-size: 15px; line-height: 1; padding: 4px;
}
.cht__messages {
  flex: 1; overflow-y: auto;
  display: flex; flex-direction: column; gap: 8px;
  padding: 14px 14px 6px;
}
.cht__msg {
  max-width: 85%;
  padding: 8px 12px;
  border-radius: 14px;
  font-size: 13.5px; line-height: 1.55;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.cht__msg--visitor { align-self: flex-end; background: var(--eng-purple); color: var(--eng-white); }
.cht__msg--ai, .cht__msg--agent { align-self: flex-start; background: rgba(255, 255, 255, 0.08); color: var(--eng-white); }
.cht__msg--agent { border: 1px solid color-mix(in srgb, var(--eng-green) 45%, transparent); }
.cht__msg--system { align-self: center; background: transparent; color: rgba(255, 255, 255, 0.72); font-size: 12px; text-align: center; }
.cht__actions { display: flex; gap: 8px; padding: 8px 14px; flex-wrap: wrap; }
.cht__action {
  flex: 1; min-width: 130px;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, var(--eng-green) 55%, transparent);
  border-radius: var(--radius-pill);
  background: transparent; cursor: pointer;
  color: var(--eng-green); font-size: 12.5px; font-weight: 700;
}
/* member mode (2026-09-05): paying members see ONE quiet link to their own
   coach instead of the sales buttons — same green-outline language as
   .cht__action so the slot reads identically, but it's an <a>, DOM-built by
   chat-widget.js (the .ejs stays byte-frozen; no bleed cycle) */
.cht__coachlink {
  display: block;
  margin: 8px 14px;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, var(--eng-green) 55%, transparent);
  border-radius: var(--radius-pill);
  color: var(--eng-green); font-size: 12.5px; font-weight: 700;
  text-align: center; text-decoration: none;
}
.cht__lead { padding: 10px 14px; border-top: 1px solid rgba(255, 255, 255, 0.12); display: flex; flex-direction: column; gap: 8px; }
.cht__lead-head { margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.8); }
.cht__consent { margin: 0; font-size: 11px; color: rgba(255, 255, 255, 0.65); }
.cht__consent a { color: var(--eng-green); text-decoration: underline; }
.cht__input {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.06);
  color: var(--eng-white); font-size: 13.5px;
}
.cht__input::placeholder { color: rgba(255, 255, 255, 0.45); }
.cht__send-lead {
  padding: 9px 12px;
  border: 2px solid var(--eng-black);
  border-radius: var(--radius-pill);
  background: var(--eng-green); color: var(--eng-ink);
  font-weight: 700; font-size: 13px; cursor: pointer;
}
/* quiet dismiss — the form must never trap the thread; same 0.8 white-alpha
   the lead-head text already passes the audit at, underlined = a control */
.cht__lead-later {
  align-self: center;
  padding: 2px 4px;
  border: 0; background: none;
  font-size: 12px; color: rgba(255, 255, 255, 0.8);
  text-decoration: underline; cursor: pointer;
}
.cht__form { display: flex; gap: 8px; padding: 10px 14px 14px; }
.cht__send {
  width: 42px; flex: none;
  border: 2px solid var(--eng-black);
  border-radius: var(--radius-pill);
  background: var(--eng-green); color: var(--eng-ink);
  font-weight: 700; font-size: 16px; cursor: pointer;
}
.cht__error {
  margin: 0 14px 12px; padding: 8px 12px;
  border-radius: var(--radius-lg);
  background: var(--eng-wrong-tint); color: var(--eng-wrong-text);
  font-size: 12.5px;
}

/* AI reply emphasis (**bold** renders as <strong> via DOM split, never
   innerHTML) — inherit the bubble ink, weight alone carries it. */
.cht__msg strong { font-weight: 700; color: inherit; }

/* typing bubble — TRUE in-flight state only (chat-widget.js mounts it when a
   request leaves, removes it when the reply/error lands) */
.cht__typing { display: inline-flex; gap: 5px; align-items: center; min-height: 18px; }
.cht__typing span {
  width: 6px; height: 6px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.65);
  animation: chtTypingBob 1.1s ease-in-out infinite;
}
.cht__typing span:nth-child(2) { animation-delay: 0.18s; }
.cht__typing span:nth-child(3) { animation-delay: 0.36s; }
@keyframes chtTypingBob {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.55; }
  30% { transform: translateY(-4px); opacity: 1; }
}

/* gift line inside the lead form — round-2 de-scam: one quiet factual
   sentence (the glowing box read as scam-bait; the reward moment lives on
   the post-submit card instead). */
.cht__lead-gift { margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.75); }
.cht__lead-gift strong { color: var(--eng-green); font-weight: 700; }

/* internal links inside AI/agent bubbles (allowlisted paths only —
   chat-widget.js appendLinkified) */
.cht__link { color: var(--eng-green); text-decoration: underline; text-underline-offset: 2px; }

/* gift card in the thread (painted by chat-widget.js after a lead lands) —
   a PRESSABLE moment: press-down feedback on :active, then the page fades
   out and the gift page fades in (the unwrap). */
.cht__giftcard {
  display: flex; gap: 10px; align-items: center; align-self: stretch;
  margin: 2px 0; padding: 10px 12px;
  border: 1.5px solid var(--eng-green);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--eng-green) 12%, transparent);
  text-decoration: none;
  box-shadow: 0 0 16px 0 color-mix(in srgb, var(--eng-green) 22%, transparent);
  cursor: pointer;
  transition: transform var(--dur-base) var(--ease-out);
}
.cht__giftcard:active { transform: scale(0.97); }
.cht__giftcard-icon { font-size: 22px; line-height: 1; }
.cht__giftcard-text { display: flex; flex-direction: column; gap: 2px; }
.cht__giftcard-title { font-weight: 700; font-size: 13px; color: var(--eng-green); }
.cht__giftcard-sub { font-size: 11.5px; color: rgba(255, 255, 255, 0.8); }

/* ── phones: the open panel becomes a BOTTOM SHEET (2026-09-18) ─────────
   Jeff's iPhone shot: the floating 360px card spilled past the right edge
   and was cut off, the page stayed live behind it, and the landing's sticky
   CTA sat on top of it. The spill was iOS zooming the page because the
   input was under 16px. So on a phone the panel stops being a floating
   card: full width, anchored to the bottom, over a dimmed page that does
   not scroll, sized to what is actually visible. MUST stay BELOW the .cht
   rules above (equal specificity; source order decides). Desktop is
   untouched. */
@media (max-width: 600px) {
  /* 16px is the floor below which iOS zooms the page on focus */
  .cht__input { font-size: 16px; }
  .cht.is-open { left: 0; right: 0; bottom: var(--cht-kb, 0px); }
  /* the backdrop is the root's own pseudo — a tap on it reaches the root,
     which chat-widget.js treats as "close" */
  .cht.is-open::before {
    content: '';
    position: fixed; inset: 0;
    background: color-mix(in srgb, var(--eng-space) 72%, transparent);
  }
  .cht.is-open .cht__panel {
    position: relative; /* paint above the ::before backdrop */
    width: 100%;
    height: min(640px, 88vh);
    height: min(640px, 88dvh, calc(var(--cht-vvh, 100dvh) - 12px));
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    border-bottom: 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  /* grab-bar: tells the eye this is a sheet over the page, not a new page */
  .cht.is-open .cht__head { position: relative; padding-top: 20px; }
  .cht.is-open .cht__head::before {
    content: '';
    position: absolute; top: 8px; left: 50%;
    width: 40px; height: 4px; margin-left: -20px;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.28);
  }
  /* 44px touch target (the ✕ was a 23px box) */
  .cht__close { min-width: 44px; min-height: 44px; margin: -10px -10px 0 0; font-size: 18px; }
  .cht__send { width: 48px; }
  body.cht-open { overflow: hidden; }
}

@media print { .cht { display: none !important; } }

/* ── chat reduced-motion tail — MUST stay BELOW the cht rules above: these
      selectors tie on specificity and source order decides (the learner.css
      tail lesson). Typing dots go static (the bubble itself is the state);
      the gift breathe dies to its rest frame. */
@media (prefers-reduced-motion: reduce) {
  .cht__typing span { animation: none; }
  .cht__giftcard { transition: none; }
  .cht__giftcard:active { transform: none; }
}
