/* =============================================================
   ENGNOVA — v3 "Signature" landing (LC-4). Page-scoped.
   Translated from docs/design-reference/handoff-v2/templates/
   landing-page-signature/LandingPageSignature.dc.html (frozen)
   → EJS + this stylesheet. Kit-local hex promoted to --eng-*
   tokens (advisor lean a: zero new hardcoded brand hex). Solid
   colours use var(--eng-*); glows / star-fields use brand-channel
   rgba() — the same convention colors_and_type.css itself uses.
   The hero + CTA star surfaces REUSE the shipped .eng-bg-space
   primitive; glow display type reuses .eng-glow-text.

   LC-4a = base + nav + hero + demo-quiz. LC-4b appends the
   section styles (orbit / ticker / pricing / CTA / footer).
   Phosphor icons are inlined as SVG in the EJS (4 icons only) —
   no CDN, no icon-font, zero CSP surface (D3 self-host spirit).
   ============================================================= */

html:has(.lp-body) { scroll-behavior: smooth; }

.lp-body {
  margin: 0;
  min-height: 100vh;
  background: var(--eng-space);
  color: var(--eng-white);
  font-family: var(--font-body-th);
  overflow-x: hidden;
}

.lp-wrap { max-width: 1240px; margin: 0 auto; }

/* Section eyebrows (.lp-eyebrow) removed 2026-09-18 at Jeff's call: the
   landing now opens each section on its heading. */

/* ---- NAV -------------------------------------------------- */
.lp-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* Full-bleed box, max-width CONTENT. A sticky bar's background has to span
     the viewport; if max-width sits on the element itself the tint stops at
     1240px and leaves a vertical seam down each side of the glass — which is
     exactly what the first draft did at 1440. */
  padding-block: 16px;
  padding-inline: max(clamp(20px, 5vw, 48px), calc((100% - 1240px) / 2));
  position: relative;
  z-index: 20;
}
/* B2: glass chrome, DESKTOP ONLY. The nav used to scroll away with the hero;
   sticky keeps the ONE CTA reachable from any point on a 5,300px page.

   Not on a phone: .lp-sticky-cta is already fixed to the bottom below 900px,
   and a sticky top bar plus a fixed bottom bar eats ~116px of an 844px screen
   — 14% of the viewport spent on chrome, on the device with the least of it.
   The bottom CTA is the better of the two there, and it is already there.

   The tint is deliberately light (52%): at scroll 0 there is nothing behind
   the nav to blur, so the tint is all you see, and at 72% it read as an opaque
   band cutting across the top of the headline. */
@media (min-width: 900px) {
  .lp-nav {
    position: sticky;
    top: 0;
    background: color-mix(in srgb, var(--eng-space) 52%, transparent);
    -webkit-backdrop-filter: blur(22px) saturate(1.6);
            backdrop-filter: blur(22px) saturate(1.6);
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  }
  /* No blur support means no glass — an untinted sticky bar would let body
     copy scroll through it and become unreadable. */
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .lp-nav { background: var(--eng-space); }
  }
}
.lp-nav__logo { display: inline-flex; align-items: center; text-decoration: none; }
.lp-nav__logo img {
  height: 46px;
  width: auto;   /* P-1 UI polish: keep the 1577×997 mark proportional (an
                    HTML width attr + this fixed height was squishing it) */
  display: block;
  filter: drop-shadow(0 0 16px rgba(255, 255, 255, 0.15));
}
.lp-nav__right { display: flex; align-items: center; gap: 20px; }
.lp-nav__links { display: flex; align-items: center; gap: 30px; }
.lp-nav__link {
  color: rgba(255, 255, 255, 0.72);
  font-weight: 500;
  font-size: 14.5px;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.lp-nav__link:hover { color: var(--eng-white); }
.lp-nav__cta {
  display: inline-flex;
  align-items: center;
  background: var(--eng-green);
  color: var(--eng-black);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 14px;
  padding: 10px 22px;
  border-radius: var(--radius-pill);
  border: 2px solid var(--eng-black);
  text-decoration: none;
  box-shadow: 0 0 22px rgba(57, 255, 137, 0.30);
  white-space: nowrap;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.lp-nav__cta:hover {
  transform: translateY(-1px);
  box-shadow: 0 0 34px rgba(57, 255, 137, 0.5);
}

/* ---- HERO = THE QUIZ -------------------------------------- */
.lp-hero {
  position: relative;
  overflow: hidden;
  padding: clamp(40px, 7vw, 72px) clamp(20px, 5vw, 48px) clamp(56px, 9vw, 96px);
}
/* ---- B2 (2026-09-01): the hero at IDLE -----------------------------------
   Measured against goldradar.in.th: 59 elements animating at rest there, 5
   here. The page did not read as broken — it read as STILL. What follows adds
   movement WITHOUT adding a single word or control, because the hero's copy
   and its one CTA are both ratified decisions (LP-CONSOLIDATE B-1 / USER 04,
   and the "light text link, never a 2nd CTA button" note at landing.ejs).

   A drifting instrument grid, under the shipped star-field rather than
   replacing it. --eng-space already carries 4 star layers and a nebula; they
   stay put, and only this grid moves, so the sky does not slide sideways. */
.lp-hero::before {
  content: "";
  position: absolute;
  inset: -60px;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
  background-size: 60px 60px;
  /* Fades out before the copy, so the grid never sits behind reading text.
     `black` is not a colour decision here — a mask reads ALPHA only, and the
     opaque stop could be any hue. Keyword rather than a hex so nobody later
     "tokenises" it into a brand colour it was never standing in for. */
  -webkit-mask-image: radial-gradient(ellipse 78% 70% at 68% 40%, black 20%, transparent 78%);
          mask-image: radial-gradient(ellipse 78% 70% at 68% 40%, black 20%, transparent 78%);
  animation: eng-drift 26s linear infinite;
}
/* Instrument lights. Six absolutely-placed dots, each on its own delay —
   in lockstep they would read as one mechanism twitching. */
.lp-hero__blips { position: absolute; inset: 0; pointer-events: none; }
.lp-hero__blips i {
  position: absolute;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--eng-green);
  box-shadow: 0 0 8px color-mix(in srgb, var(--eng-green) 70%, transparent);
}
.lp-hero__blips i:nth-child(1) { left: 61%; top: 16%; animation-delay: 0s; }
.lp-hero__blips i:nth-child(2) { left: 88%; top: 34%; animation-delay: .7s; background: var(--eng-yellow); box-shadow: 0 0 8px color-mix(in srgb, var(--eng-yellow) 70%, transparent); }
.lp-hero__blips i:nth-child(3) { left: 72%; top: 68%; animation-delay: 1.5s; }
.lp-hero__blips i:nth-child(4) { left: 93%; top: 74%; animation-delay: 2.2s; background: var(--eng-purple); box-shadow: 0 0 9px color-mix(in srgb, var(--eng-purple) 80%, transparent); }
.lp-hero__blips i:nth-child(5) { left: 55%; top: 87%; animation-delay: 2.9s; }
.lp-hero__blips i:nth-child(6) { left: 79%; top: 8%;  animation-delay: 3.6s; background: var(--eng-purple); box-shadow: 0 0 9px color-mix(in srgb, var(--eng-purple) 80%, transparent); }
.lp-hero__grid {
  position: relative;
  display: grid;
  /* 1.35, not 1.1: at 1.1 the headline needed 4 lines and the last one was the
     single orphan word "จุดนั้น". text-wrap balance/pretty do not fix it —
     measured, all three wrap modes give the same 4 lines — because the copy
     genuinely does not fit in ~600px. Width is the lever. Saturn goes 452 →
     432px, still above its own 400px design size. */
  grid-template-columns: 1.35fr 0.65fr;
  align-items: center;
  max-width: 1240px;
  margin: 0 auto;
  gap: clamp(32px, 5vw, 64px);
}
/* D2: Thai has no spaces between words, so the browser may break a phrase
   anywhere — at 1280 it broke "ไม่รู้ว่าอ่อนตรงไหน ก็ / ไม่รู้จะเริ่มตรงไหน",
   splitting "ก็" from the clause it opens. Each phrase is one unbreakable
   unit; lines break only BETWEEN phrases. */
.lp-nw { white-space: nowrap; }

/* D2 desktop hero: copy left, demo right, both in the first viewport. The
   copy wrapper dissolves (display:contents) so every child is placed here by
   area — DOM order, mobile layout and the scripts that find these by id are
   untouched. Saturn sits BEHIND the opaque demo panel and peeks from its top
   corner: the destination behind the lesson, instead of a lone planet in an
   empty half.
   ⚠️ 1180px, not 901: the phrases are unbreakable, so the copy column must fit
   "ก็ไม่รู้จะเริ่มตรงไหน" at 56px (~445px). At 1024 a 560px demo left ~330px
   and the headline ran UNDER the card — hidden, so page overflow read 0.
   Below 1180 the stacked layout (demo under the copy) stays. */
@media (min-width: 1180px) {
  .lp-hero__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 560px);
    grid-template-areas:
      "h1     stage"
      "sub    stage"
      "cue    stage"
      "sat    stage"
      ".      democta";
    grid-template-rows: auto auto auto 1fr auto;
    column-gap: clamp(40px, 5vw, 72px);
    row-gap: 0;
    align-items: start;
  }
  .lp-hero__copy { display: contents; }
  .lp-hero__grid .lp-hero__h1 { grid-area: h1; }
  .lp-hero__sub { grid-area: sub; }
  .lp-hero__cue { grid-area: cue; align-self: start; margin-top: 8px; justify-self: start; }
  .lp-hero__grid .lp-flagship-demo { grid-area: stage; margin: 0; position: relative; z-index: 1; width: 100%; }
  .lp-hero__grid .lp-demo-cta { grid-area: democta; justify-self: center; }
  /* Jeff (2026-09-18, after seeing it hidden behind the opaque card): the
     planet goes in the empty space under the copy, whole and unobstructed.
     It fills the void the two-column layout opened on the left instead of
     competing with the demo on the right. */
  .lp-hero__grid .lp-sat-wrap {
    grid-area: sat;
    align-self: start; justify-self: start;
    width: 220px;
    margin-top: 28px;
    pointer-events: none;
  }
}

.lp-hero__h1 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(34px, 6.5vw, 56px);
  line-height: 1.1;
  letter-spacing: -0.015em;
  margin: 0 0 12px;
  color: var(--eng-white);
  /* Without this the last line was the single orphan word "จุดนั้น". balance
     works per line-box group, so the explicit <br> in the markup still holds
     where the copy wants a hard break. */
  text-wrap: balance;
}
/* The ONE highlighted word gets light travelling through it — scoped to the
   hero h1, never to the shared .eng-glow-text (12 elements across 9 views).
   The glow stays: it is what carries the word on a dark ground, and the
   shimmer only moves the highlight inside it. */
.lp-hero__h1 .eng-glow-text {
  background-image: linear-gradient(100deg,
    var(--eng-green) 0%,
    color-mix(in srgb, var(--eng-green) 55%, var(--eng-white)) 45%,
    var(--eng-green) 90%);
  background-size: 220% auto;
  -webkit-background-clip: text;
          background-clip: text;
  animation: eng-shimmer 5s ease-in-out infinite;
}
/* S1 subhead (A-3) — the hero's value-prop line, kept tight so the playable
   demo stays above the fold on mobile. */
.lp-hero__sub {
  font-size: clamp(16px, 2.5vw, 21px);
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.9);
  margin: 0 0 22px;
  max-width: 540px;
}
.lp-hero__sub b { color: var(--eng-white); font-weight: 700; }
.lp-hero__cue {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 44px;
  color: rgba(255, 255, 255, 0.5);
  font-size: 13.5px;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.lp-hero__cue:hover { color: var(--eng-white); }
.lp-hero__cue svg { animation: lp-cue 1.8s ease-in-out infinite; }

.lp-sat-wrap { display: flex; align-items: center; justify-content: center; }
.lp-sat {
  width: min(100%, 400px);
  height: auto;
  filter: drop-shadow(0 0 50px rgba(57, 255, 137, 0.24));  /* green aura, not yellow */
  animation: lp-sat-float 8s ease-in-out infinite;
}
/* De-yellowed per brand law (yellow = restrained micro-accent, never a large
   field) + GREEN glowing ring = the Saturn persona signature (character-
   consistency ledger: green glowing ring ALWAYS). Body = luminous cool-neutral
   to contrast the purple nebula; a single tiny yellow glint = the one permitted
   micro spark. Ring/body/craters/stops all via tokens + brand-channel rgba. */
.lp-sat__ring   { fill: none; stroke: var(--eng-green); filter: drop-shadow(0 0 7px rgba(57, 255, 137, 0.7)); }
.lp-sat__body   { stroke: var(--eng-black); }
.lp-sat__crater { fill: rgba(0, 0, 0, 0.12); }
.lp-sat__front  { fill: none; stroke: var(--eng-green); filter: drop-shadow(0 0 7px rgba(57, 255, 137, 0.7)); }
.lp-sat__glint  { fill: var(--eng-yellow); filter: drop-shadow(0 0 4px rgba(255, 242, 0, 0.6)); }
.lp-sat__stop0  { stop-color: color-mix(in srgb, var(--eng-white) 82%, var(--eng-silver)); }
.lp-sat__stop1  { stop-color: var(--eng-silver); }
.lp-sat__stop2  { stop-color: var(--eng-slate); }
/* 5 ring markers = the five SKILLS in orbit, uniform. No "now"/first marker:
   an ordered ladder contradicted the test-pinned line at landing.ejs:184. */
.lp-sat__marker      { fill: color-mix(in srgb, var(--eng-white) 55%, transparent); }

@keyframes lp-sat-float {
  0%, 100% { transform: rotate(-8deg) translateY(0); }
  50%      { transform: rotate(-8deg) translateY(-14px); }
}
/* B2: the ring pulses its own glow instead of holding one fixed drop-shadow.
   drop-shadow, not box-shadow — an SVG stroke has no box to cast one, and a
   box-shadow here would draw a rectangle around the whole illustration. */
@keyframes lp-sat-ring {
  0%, 100% { filter: drop-shadow(0 0 7px color-mix(in srgb, var(--eng-green) 70%, transparent)); }
  50%      { filter: drop-shadow(0 0 15px color-mix(in srgb, var(--eng-green) 95%, transparent)); }
}
.lp-sat__ring, .lp-sat__front { animation: lp-sat-ring 4.6s ease-in-out infinite; }
.lp-sat__front { animation-delay: .35s; }
/* The yellow glint is the one loud micro-accent on this illustration; it
   blinks on its own clock so the planet does not pulse as a single unit. */
.lp-sat__glint { animation: eng-blip 5.2s ease-in-out infinite; animation-delay: 1.1s; }
@keyframes lp-cue {
  0%, 100% { transform: translateY(0); opacity: 0.5; }
  50%      { transform: translateY(6px); opacity: 1; }
}

/* ---- HERO DEMO — flagship lesson runner (LP-REBUILD A-2) ---------------
   The hero runs the REAL forked runner (public/js/lesson-flow-flagship.js) +
   flagship.css scene games. flagship.css styles the fsm/fsd cards; the runner's
   standard chrome (lf-card / lf-nav / lf-progress / lf-choice / lf-visual)
   lives in learner.css, which the marketing landing does NOT load — so a minimal
   token-only subset is scoped here (verbatim values from learner.css; zero new
   hex). Content drift is guarded by the route import + the tests/routes/
   landing.test.js drift pin; this block is presentation only. */
.lp-flagship-demo {
  max-width: 560px;
  margin: 22px 0 0;
  padding: 18px 16px 14px;
  border: 1.5px solid rgba(255, 255, 255, 0.12);
  border-radius: 20px;
  /* OPAQUE deep-space panel — the runner's text (incl. the gate hint that sits
     on the panel bg, outside .lf-card) must not read the hero background through
     a translucent surface (contrast fail @390 where a light hero element bleeds
     under it). Opaque = every glyph is guaranteed on --eng-space. */
  background: var(--eng-space);
  /* B2: the demo is the hero's star (that is WHY there is no loud hero button
     — see the note at landing.ejs). A still dark panel reads as a screenshot
     of a lesson; a breathing edge reads as one waiting to be played. The ring
     is the element's own border colour, so it costs no extra layer. */
  transition: border-color var(--dur-base) var(--ease-out);
  animation: lp-demo-ready 4.2s ease-in-out infinite;
}
@keyframes lp-demo-ready {
  0%, 100% { border-color: rgba(255, 255, 255, 0.12);
             box-shadow: 0 0 26px color-mix(in srgb, var(--eng-green) 8%, transparent); }
  50%      { border-color: color-mix(in srgb, var(--eng-green) 34%, transparent);
             box-shadow: 0 0 40px color-mix(in srgb, var(--eng-green) 20%, transparent); }
}
/* Once a learner touches it the invitation has been accepted — a panel that
   keeps asking to be played WHILE being played is nagging. */
.lp-flagship-demo:hover, .lp-flagship-demo:focus-within { animation-play-state: paused; border-color: color-mix(in srgb, var(--eng-green) 40%, transparent); }
.lp-flagship-demo .lf-stage { min-height: 340px; display: flex; align-items: stretch; }
.lp-flagship-demo .lf-card {
  flex: 1; padding: 20px 18px;
  border: 1.5px solid rgba(255, 255, 255, 0.10);
  border-radius: 18px;
  background: color-mix(in srgb, var(--eng-white) 6%, transparent);
}
.lp-flagship-demo .lf-card__eyebrow { font-size: 0.85rem; color: var(--eng-white); opacity: 0.75; margin-bottom: 10px; }
.lp-flagship-demo .lf-card__title { font-family: var(--font-display); font-weight: 700; font-size: 1.35rem; color: var(--eng-white); margin-bottom: 10px; }
.lp-flagship-demo .lf-card__sub { color: var(--eng-white); opacity: 0.85; margin-bottom: 14px; }

.lp-flagship-demo .lf-enter { animation: lf-in 0.34s cubic-bezier(0.22, 1, 0.36, 1); }
.lp-flagship-demo .lf-enter-back { animation: lf-in-back 0.3s cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes lf-in { from { opacity: 0; transform: translateX(26px); } to { opacity: 1; transform: none; } }
@keyframes lf-in-back { from { opacity: 0; transform: translateX(-26px); } to { opacity: 1; transform: none; } }

.lp-flagship-demo .lf-nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 18px; }
.lp-flagship-demo .lf-progress { display: flex; flex-direction: column; align-items: center; gap: 5px; flex: 1; min-width: 0; }
.lp-flagship-demo .lf-progress__label { font-size: 0.8rem; color: var(--eng-white); opacity: 0.7; }
.lp-flagship-demo .lf-progress__track { display: block; width: min(180px, 100%); height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.14); overflow: hidden; }
.lp-flagship-demo .lf-progress__bar { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--eng-green); transition: width 0.3s ease; }
.lp-flagship-demo .lf-gate-hint { text-align: center; margin-top: 10px; font-size: 0.9rem; color: var(--eng-white); opacity: 0.75; }
.lp-flagship-demo .lf-gate-hint[hidden] { display: none; }

.lp-flagship-demo .lf-predict { margin-top: 14px; }
.lp-flagship-demo .lf-predict__ask { font-weight: 700; color: var(--eng-white); margin-bottom: 8px; }
.lp-flagship-demo .lf-choice { display: grid; gap: 10px; margin: 12px 0; }
.lp-flagship-demo .lf-choice__btn {
  text-align: left; font-size: 1.05rem; color: var(--eng-white);
  background: rgba(255, 255, 255, 0.05); border: 1.5px solid rgba(255, 255, 255, 0.16);
  border-radius: 14px; padding: 14px 16px; cursor: pointer;
  transition: border-color 0.18s, background 0.18s, transform 0.18s;
}
.lp-flagship-demo .lf-choice__btn:hover:not(:disabled) { border-color: color-mix(in srgb, var(--eng-purple) 70%, white 12%); }
.lp-flagship-demo .lf-choice__btn:disabled { cursor: default; opacity: 0.55; }
.lp-flagship-demo .lf-choice__btn.is-right {
  border-color: var(--eng-green);
  background: color-mix(in srgb, var(--eng-green) 16%, transparent);
  color: var(--eng-green); opacity: 1;
}
.lp-flagship-demo .lf-choice__btn.is-wrong-pick {
  border-color: var(--eng-red-on-dark);
  background: color-mix(in srgb, var(--eng-red-on-dark) 14%, transparent);
  color: var(--eng-red-on-dark); opacity: 1;
}
.lp-flagship-demo .lf-fsm__feedback {
  margin-top: 10px; font-size: var(--fs-body-sm); line-height: 1.55;
  color: var(--eng-white); background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline-dark); border-radius: 10px; padding: 10px 14px;
}
.lp-flagship-demo .lf-fsm__feedback--ok { border-color: color-mix(in srgb, var(--eng-green) 45%, transparent); color: var(--eng-green); font-weight: 700; }
.lp-flagship-demo .lf-fsm__feedback[hidden] { display: none; }

.lp-flagship-demo .lf-iv__ask { font-weight: 700; color: var(--eng-white); margin-bottom: 12px; }
.lp-flagship-demo .lf-visual__title { font-family: var(--font-display); font-weight: 700; color: var(--eng-white); margin-bottom: 12px; text-align: center; }
.lp-flagship-demo .lf-visual__caption { font-size: 0.9rem; color: var(--eng-white); opacity: 0.8; margin-top: 12px; text-align: center; }

/* E1: light green text-link below the runner (NOT a loud button — the playable
   demo is the hero's CTA; the picker is the next loud moment). The ↓ points down
   to #picker. */
.lp-demo-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 18px;
  color: var(--eng-green);
  font-weight: 600;
  font-size: 14px;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.lp-demo-cta:hover { color: var(--eng-white); }
.lp-demo-cta svg { width: 1em; height: 1em; }

@media (max-width: 900px) {
  .lp-flagship-demo { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .lp-flagship-demo .lf-enter, .lp-flagship-demo .lf-enter-back { animation: none; }
  .lp-flagship-demo .lf-progress__bar, .lp-flagship-demo .lf-choice__btn { transition: none; }
}

/* ---- SHARED (sections) ------------------------------------ */
.lp-i { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; flex-shrink: 0; }
.lp-sec { padding: clamp(56px, 10vw, 96px) clamp(20px, 5vw, 48px); border-top: 1px solid rgba(255, 255, 255, 0.06); }
.lp-h2 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(30px, 5.5vw, 44px);
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--eng-white);
  margin: 0 0 8px;
  text-wrap: balance;
}
/* B3 reveal. `is-out` is added by JS ONLY to blocks below the fold, so the
   default — and the JS-off, reduced-motion, and already-visible states — is
   the finished page. Nothing here can leave content invisible.
   The stagger comes from --i on each child; 90ms apart, per MOTION-LANGUAGE. */
.lp-reveal {
  transition: opacity var(--dur-enter) var(--ease-out),
              transform var(--dur-enter) var(--ease-out);
  transition-delay: calc(var(--i, 0) * 90ms);
}
.lp-reveal.is-out { opacity: 0; transform: translateY(18px); }

/* B2 side-effect: a sticky nav swallows the top of whatever an anchor link
   jumps to. Every in-page target reserves the nav's height. Desktop-only,
   because the nav is only sticky there. */
@media (min-width: 900px) {
  .lp-sec, .lp-hero, #map, #how, #picker, #day, #placement { scroll-margin-top: 84px; }
}

.lp-sec__lead { font-size: 15.5px; color: rgba(255, 255, 255, 0.6); margin: 0 0 40px; max-width: 540px; line-height: 1.6; }

/* ---- MAP (B-2: real first lessons per door; replaced #orbit) ---- */
/* ---- #map · the claim, then the fleet -------------------------------
   BEAT 1 — the selling point had to get LOUDER (Jeff, 2026-09-01, second pass
   on the rendered page). It is now larger than a section heading and the claim
   half carries the same green .eng-glow-text the hero uses for its promise, so
   a reader scanning the page meets the same emphasis channel twice.
   BEAT 2 — the craft are the REAL renders from public/img/skill-marks, the same
   five the article hero ships, each pulling a beam toward Saturn. */
.lp-usp {
  font-size: clamp(34px, 6.4vw, 56px);
  max-width: 22ch;
  margin-bottom: 10px;
}
/* the claim half must wrap as ONE phrase — at 16ch it broke as "จุดที่ / คุณพลาด",
   which splits the very words the sentence exists to land */
.lp-usp .eng-glow-text { display: inline-block; }
.lp-usp__counter {
  font-size: clamp(16px, 2.2vw, 21px);
  color: rgba(255, 255, 255, 0.62);
  margin: 0;
}

/* #map is the hero of the page (Jeff, 2026-09-01), so it gets sky and room.
   The shipped .eng-bg-space star field is a fixed background; a SECOND faint
   layer drifting at its own rate is what turns a flat backdrop into depth, and
   it stays scoped here rather than touching the frozen kit surface. */
.lp-map { position: relative; padding-block: clamp(76px, 12vw, 132px); }
.lp-map::before {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 12% 62%, rgba(255, 255, 255, 0.30) 1px, transparent 1.8px),
    radial-gradient(circle at 74% 34%, rgba(255, 255, 255, 0.22) 1px, transparent 1.6px),
    radial-gradient(circle at 44% 86%, rgba(201, 178, 255, 0.28) 1px, transparent 1.8px);
  background-size: 210px 210px, 270px 270px, 330px 330px;
  animation: lp-map-drift 90s linear infinite;
}
@keyframes lp-map-drift {
  to { background-position: 210px -210px, -270px 270px, 330px 330px; }
}
.lp-map > .lp-wrap { position: relative; }

/* This one line carries the whole metaphor, so it is set as a sentence rather
   than as a label. The uppercase + 0.08em tracking it used to carry does
   nothing for Thai (there is no case, and extra tracking pulls apart the
   clusters a reader parses as one shape). */
.lp-fleet__intro {
  margin: 44px 0 12px;
  font-size: 15.5px;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.68);
}
.lp-fleet__intro b { color: var(--eng-white); }
.lp-fleet {
  padding: clamp(16px, 3vw, 34px) 0;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 clamp(6px, 1.2vw, 18px);
  align-items: center;
}
.lp-fleet__lanes { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 20px; min-width: 0; }
/* text left, motion right — one row per skill. The two-row version made each
   lane 90px tall and put the words a long way from the craft they describe. */
.lp-lane {
  display: grid;
  grid-template-columns: minmax(260px, 36%) 1fr;
  gap: 0 clamp(12px, 2vw, 24px);
  align-items: center;
  min-width: 0;
  /* Each craft flies in its OWN colour — the same five the article heroes use,
     imported from one table so a beam and the craft it trails cannot disagree.
     Five green beams were also a large green field, and green is the action
     channel in this system, not a decorative one. */
  --lane-hue: var(--eng-green);
}
.lp-lane__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.lp-lane__name { font-family: var(--font-display); font-weight: 700; font-size: 17px; color: var(--eng-white); }
.lp-lane__payoff { font-size: 14px; line-height: 1.55; color: rgba(255, 255, 255, 0.64); }
/* The five tracks converge on the destination. Parallel rails of equal length
   read as a chart; the same five rails aimed at one point read as flight paths,
   and the craft bank with them because the whole lane rotates. The pivot is the
   PAD (left centre) so every craft still departs from the same line — the tilt
   is about where they are going, never about how far along they are. Lengths
   stay identical: an unequal lane would be a progress claim, and this section
   carries no numbers by design. */
.lp-lane__track {
  position: relative; height: 66px; min-width: 0;
  transform-origin: left center;
  rotate: calc((2 - var(--i, 2)) * 1.7deg);
}
/* the rail the beam is drawn on — it reaches past the column so the five lanes
   arrive AT the planet instead of stopping short of it */
.lp-lane__track::before {
  content: "";
  position: absolute;
  left: 0; right: calc(-1 * clamp(6px, 1.2vw, 18px)); top: 50%;
  height: 1px; margin-top: -0.5px;
  background: linear-gradient(90deg,
    rgba(255, 255, 255, 0.04) 0%,
    rgba(255, 255, 255, 0.13) 100%);
}
/* the pad each craft leaves from — five hairlines starting at nothing read as
   a chart; a mark at the origin makes them journeys with a departure point */
.lp-lane__track::after {
  content: "";
  position: absolute;
  left: 0; top: 50%; width: 2px; height: 13px; margin-top: -6.5px;
  border-radius: 1px;
  background: color-mix(in srgb, var(--lane-hue) 45%, transparent);
}
/* ลำแสง — the beam the craft drags behind it. Width tracks the craft, so it
   reads as thrust rather than as a progress bar someone filled in. */
.lp-lane__beam {
  position: absolute;
  left: 0; top: 50%; height: 2px; margin-top: -1px;
  width: 100%;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--lane-hue) 12%, transparent) 0%,
    color-mix(in srgb, var(--lane-hue) 55%, transparent) 45%,
    var(--lane-hue) 100%);
  border-radius: var(--radius-pill);
  filter: drop-shadow(0 0 8px color-mix(in srgb, var(--lane-hue) 55%, transparent));
}
.lp-lane__craft {
  position: absolute;
  left: 100%; top: 50%;
  width: clamp(52px, 5vw, 68px); height: clamp(52px, 5vw, 68px);
  margin: calc(clamp(52px, 5vw, 68px) / -2) 0 0 calc(clamp(52px, 5vw, 68px) / -2);
  object-fit: contain;
  filter: drop-shadow(0 0 14px color-mix(in srgb, var(--lane-hue) 55%, transparent));
}
/* armed = JS has pulled the fleet back to the pad; launched = the section has
   been seen. With no JS neither class exists and everything rests ARRIVED, so a
   reader without it sees a finished fleet rather than five craft on a runway. */
.lp-fleet.is-armed .lp-lane__craft { left: 0; }
.lp-fleet.is-armed .lp-lane__beam { width: 0; }
.lp-fleet.is-armed.is-launched .lp-lane__craft,
.lp-fleet.is-armed.is-launched .lp-lane__beam {
  transition: left 1250ms var(--ease-out), width 1250ms var(--ease-out);
  transition-delay: calc(var(--i, 0) * 130ms);
}
.lp-fleet.is-armed.is-launched .lp-lane__craft { left: 100%; }
.lp-fleet.is-armed.is-launched .lp-lane__beam { width: 100%; }
/* Once landed the fleet used to freeze forever, on the section that carries the
   most weight on the page. The beam keeps a slow drift afterwards — ambient,
   and only after the arrival transition has finished. */
.lp-fleet.is-armed.is-launched .lp-lane__beam {
  animation: lp-beam-drift 4.2s var(--ease-in-out) infinite;
  animation-delay: calc(1700ms + var(--i, 0) * 130ms);
}
@keyframes lp-beam-drift {
  0%, 100% { opacity: .78; }
  50%      { opacity: 1; }
}

.lp-fleet__dest { display: grid; place-items: center; }
.lp-fleet__sat { width: clamp(170px, 23vw, 310px); height: auto; overflow: visible; }
.sat-hi  { stop-color: color-mix(in srgb, var(--eng-purple) 42%, var(--eng-white)); }
.sat-mid { stop-color: var(--eng-purple); }
.sat-lo  { stop-color: color-mix(in srgb, var(--eng-purple) 45%, var(--eng-space)); }
.halo-in  { stop-color: color-mix(in srgb, var(--eng-purple) 26%, transparent); }
.halo-out { stop-color: transparent; }
.lp-fleet__halo { animation: lp-sat-breathe 6s var(--ease-in-out) infinite; }
@keyframes lp-sat-breathe { 0%, 100% { opacity: .62; } 50% { opacity: 1; } }
.lp-fleet__ring {
  fill: none; stroke: var(--eng-green); stroke-width: 2.4; stroke-linecap: round;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--eng-green) 55%, transparent));
}
.lp-fleet__ring--back  { opacity: .42; }   /* the far half, dimmed by the planet */
.lp-fleet__ring--front { opacity: 1; }
.lp-fleet__body { stroke: color-mix(in srgb, var(--eng-purple) 70%, transparent); stroke-width: 1.2; }
/* the night side. A gradient sphere still reads flat without one. */
.lp-fleet__term { fill: color-mix(in srgb, var(--eng-space) 55%, transparent); }
.lp-fleet__moonorbit {
  transform-box: view-box; transform-origin: 100px 100px;
  animation: lp-moon 22s linear infinite;
}
@keyframes lp-moon { to { transform: rotate(360deg); } }
.lp-fleet__moon {
  fill: var(--eng-purple-soft);
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--eng-purple-soft) 60%, transparent));
}
/* the planet answers when the last craft lands (5th lane: 4 × 130ms + 1250ms) */
@keyframes lp-sat-arrive {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.045); }
  100% { transform: scale(1); }
}
.lp-fleet.is-armed.is-launched .lp-fleet__sat {
  animation: lp-sat-arrive 1100ms var(--ease-out) 1650ms both;
}
/* the arrival itself — one ring of light leaving the planet as the last craft
   docks. It fires ONCE (no infinite), so it reads as an event, not decoration. */
.lp-fleet__wave { fill: none; stroke: var(--eng-green); stroke-width: 1.2; opacity: 0;
                  transform-box: view-box; transform-origin: 100px 100px; }
/* 1800ms = the last craft's own arrival (1250ms flight + 4 x 130ms stagger).
   Firing earlier made the planet answer a ship that had not landed yet. */
.lp-fleet.is-armed.is-launched .lp-fleet__wave {
  animation: lp-sat-wave 1300ms var(--ease-out) 1800ms both;
}
@keyframes lp-sat-wave {
  0%   { opacity: .8; transform: scale(0.62); }
  100% { opacity: 0;  transform: scale(2.05); }
}

.lp-map__h3 {
  margin: 60px 0 0;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.5);
}

/* ---- #map BEAT 3 · the flight console ---------------------------------
   The five craft above are flown from somewhere. This is that somewhere: a nav
   console with three route selectors and one readout screen. Jeff, 2026-09-02:
   "เหมือนห้องขับยานอวกาศ … เราสามารถเลือกเส้นทางได้ว่าจะเลือกทางไหน".

   Default (no JS, reduced motion): every panel visible under its own heading,
   no selectors — the layout this replaced. `is-armed` switches to one at a
   time and hands the heading job to the lit selector. */
.lp-console { margin-top: clamp(26px, 3.5vw, 44px); }
.lp-console__bezel {
  position: relative;
  padding: clamp(18px, 2.6vw, 30px);
  border-radius: 18px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  /* the housing: a lit top edge, a deep well, and the section's own sky faintly
     visible through it — a flat card would read as a card, not as hardware */
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012) 38%, transparent 70%),
    rgba(11, 7, 22, 0.55);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.09),
    inset 0 0 60px rgba(99, 53, 240, 0.10),
    0 24px 60px rgba(0, 0, 0, 0.35);
}
/* HUD corner brackets. Four L-shapes, two borders each — the cheapest mark that
   says "instrument", and it costs no image. */
.lp-console__corner {
  position: absolute; width: 16px; height: 16px; pointer-events: none;
  border: 0 solid color-mix(in srgb, var(--eng-green) 55%, transparent);
}
.lp-console__corner--tl { top: 9px;    left: 9px;    border-top-width: 1.5px;    border-left-width: 1.5px;  border-top-left-radius: 6px; }
.lp-console__corner--tr { top: 9px;    right: 9px;   border-top-width: 1.5px;    border-right-width: 1.5px; border-top-right-radius: 6px; }
.lp-console__corner--bl { bottom: 9px; left: 9px;    border-bottom-width: 1.5px; border-left-width: 1.5px;  border-bottom-left-radius: 6px; }
.lp-console__corner--br { bottom: 9px; right: 9px;   border-bottom-width: 1.5px; border-right-width: 1.5px; border-bottom-right-radius: 6px; }

.lp-console__head {
  display: flex; align-items: center; gap: 10px;
  padding-bottom: 14px; margin-bottom: 18px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.09);
}
.lp-console__lamp {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--eng-green);
  box-shadow: 0 0 10px color-mix(in srgb, var(--eng-green) 75%, transparent);
  animation: lp-console-lamp 2.6s var(--ease-in-out) infinite;
}
@keyframes lp-console-lamp { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
.lp-console__title {
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
  color: var(--eng-white);
}
/* the only Latin on the panel, and it is instrument chrome rather than copy —
   aria-hidden, so it is never read aloud to someone who came for Thai.
   🚫 NOT color-mix: it computes to color(srgb …) and every rgb()-only parser
   reads that as black — including our own contrast audit, which failed this
   line at 1.12:1 on its first run. Green also belongs to action in this system,
   and a readout is not an action; the lamp beside it carries the status. */
.lp-console__status {
  margin-left: auto;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 11px; letter-spacing: 0.14em;
  color: rgba(255, 255, 255, 0.55);                          /* audit:contrast */
}

/* ---- the three route selectors ---- */
.lp-console__routes { display: none; }
.lp-console.is-armed .lp-console__routes {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: clamp(8px, 1.2vw, 14px);
  margin-bottom: clamp(16px, 2.2vw, 24px);
}
.lp-console__route {
  display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 11px;
  text-align: left; cursor: pointer;
  min-height: 62px;
  padding: 14px 17px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.11);
  /* a switch has a lit top edge and a body that falls away under it; a flat
     fill is what made the first pass read as three tabs */
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.012));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
  color: rgba(255, 255, 255, 0.72);                          /* audit:contrast */
  font: inherit; font-size: 13.5px; line-height: 1.45;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.lp-console__route:hover { color: var(--eng-white); border-color: rgba(255, 255, 255, 0.24); }
.lp-console__route:focus-visible { outline: 2px solid var(--eng-green); outline-offset: 2px; }
/* the selected heading: the switch is thrown, the lamp is lit, the label is
   full-strength white. Everything else on the row stays quiet. */
.lp-console__route.is-on {
  color: var(--eng-white);
  border-color: color-mix(in srgb, var(--eng-green) 50%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--eng-green) 22%, transparent),
              inset 0 0 24px color-mix(in srgb, var(--eng-green) 13%, transparent),
              0 0 20px color-mix(in srgb, var(--eng-green) 18%, transparent);
}
.lp-console__led {
  flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.26);
  transition: background var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.lp-console__route.is-on .lp-console__led {
  background: var(--eng-green);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--eng-green) 16%, transparent),
              0 0 14px color-mix(in srgb, var(--eng-green) 60%, transparent);
}

/* ---- the readout screen ---- */
.lp-console__screen {
  position: relative; overflow: hidden;
  border-radius: 14px;
  padding: clamp(18px, 2.4vw, 28px) clamp(18px, 2.4vw, 28px) clamp(18px, 2.4vw, 28px)
           calc(clamp(18px, 2.4vw, 28px) + 16px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  /* a screen, not a card: a darker well with a faint instrument grid */
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--eng-green) 5%, transparent), transparent 34%),
    repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.022) 0 1px, transparent 1px 28px),
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.022) 0 1px, transparent 1px 28px),
    rgba(0, 0, 0, 0.34);
  /* the glass sits INSIDE the panel — without the well it reads as one more
     card stacked on another card */
  box-shadow: inset 0 2px 14px rgba(0, 0, 0, 0.55),
              inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}
/* one slow pass of light down the glass. Ambient — it has no end state, so
   reduced motion simply stops it. */
.lp-console__scan {
  position: absolute; left: 0; right: 0; top: 0; height: 90px;
  pointer-events: none;
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--eng-green) 7%, transparent), transparent);
  animation: lp-console-scan 7s var(--ease-in-out) infinite;
}
@keyframes lp-console-scan { 0% { transform: translateY(-100%); } 100% { transform: translateY(900%); } }
/* an instrument scale down the left edge of the glass. Pure chrome: it carries
   no reading, which is why it carries no numbers either. */
.lp-console__screen::before {
  content: "";
  position: absolute; left: 0; top: 12px; bottom: 12px; width: 9px;
  pointer-events: none;
  border-right: 1px solid rgba(255, 255, 255, 0.07);
  background: repeating-linear-gradient(180deg,
    rgba(255, 255, 255, 0.16) 0 1px, transparent 1px 13px);
}
.lp-console__cap { display: none; }
.lp-console.is-armed .lp-console__cap {
  display: block; position: relative;
  margin: 0 0 14px;
  font-size: 12px; letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.55);                          /* audit:contrast */
}
.lp-console__panels { position: relative; }
.lp-console__panel { position: relative; }
.lp-console__panel + .lp-console__panel { margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255, 255, 255, 0.07); }
.lp-console__panelh {
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
  margin: 0 0 14px; color: var(--eng-white); line-height: 1.4;
}
/* armed: the lit selector names the route, so the readout must not say it a
   second time — that duplication is what this beat exists to remove */
.lp-console.is-armed .lp-console__panels { display: grid; }
.lp-console.is-armed .lp-console__panel {
  grid-area: 1 / 1;
  margin-top: 0; padding-top: 0; border-top: 0;
  visibility: hidden; opacity: 0;
}
.lp-console.is-armed .lp-console__panel.is-on {
  visibility: visible; opacity: 1;
  animation: lp-console-in 420ms var(--ease-out) both;
}
.lp-console.is-armed .lp-console__panelh { display: none; }
@keyframes lp-console-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.lp-console__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 16px; }
.lp-console__item { display: flex; gap: 12px; align-items: flex-start; }
/* a waypoint marker, not a bullet: a small chevron pointing along the route */
.lp-console__wp {
  flex: none; width: 6px; height: 6px; margin-top: 6px;
  border-top: 1.5px solid var(--eng-green);
  border-right: 1.5px solid var(--eng-green);
  transform: rotate(45deg);
}
.lp-console__t { font-size: 15.5px; font-weight: 600; color: var(--eng-white); line-height: 1.5; }

.lp-map__foot { margin-top: 22px; font-size: 13.5px; color: rgba(255, 255, 255, 0.6); }

/* ---- HOW IT WORKS (B-3: 3-step mechanism + secondary report block) ---- */
.lp-how__steps { display: flex; flex-direction: column; gap: 18px; margin: 28px auto 0; max-width: 720px; text-align: left; }
/* B4 (2026-09-01): #how is a MECHANISM, so it is drawn as one.
   Measured before this: #how 0 · #picker 0 · #day 0 · #placement 0 elements
   animating at rest — 2,883px of page where nothing moved, while the hero
   carried 14. Hover states and a one-shot reveal are not ambient; they are
   motion that only exists if you touch or arrive.

   A spine runs behind the three nodes and a signal travels down it, then the
   nodes light in the same order. That is the claim the section makes — an
   answer becomes evidence becomes a changed plan — so the motion IS the
   content, not decoration laid over it. */
.lp-how__steps { position: relative; }
.lp-how__spine {
  position: absolute;
  left: 33px; top: 34px; bottom: 34px; width: 2px;
  pointer-events: none;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--glow-accent) 34%, transparent),
    color-mix(in srgb, var(--glow-accent) 10%, transparent));
}
/* The signal itself: a short bright segment sliding the length of the spine. */
.lp-how__spine::after {
  content: "";
  position: absolute;
  left: -1px; width: 4px; height: 42px; border-radius: var(--radius-pill);
  background: linear-gradient(to bottom, transparent, var(--glow-accent), transparent);
  box-shadow: 0 0 12px color-mix(in srgb, var(--glow-accent) 80%, transparent);
  animation: lp-signal 5.4s var(--ease-in-out) infinite;
}
@keyframes lp-signal {
  0%        { top: -42px; opacity: 0; }
  8%        { opacity: 1; }
  75%       { top: 100%; opacity: 1; }
  85%, 100% { top: 100%; opacity: 0; }
}

.lp-how__step {
  position: relative;
  display: flex; gap: 16px; align-items: flex-start;
  padding: 14px 16px;
  border-radius: 16px;
  border: 1.5px solid transparent;
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
.lp-how__step:hover {
  border-color: color-mix(in srgb, var(--glow-accent) 24%, transparent);
  background: color-mix(in srgb, var(--glow-accent) 4%, transparent);
}
/* Each node fires as the signal reaches it — same 5.4s clock, one third apart,
   so the sequence reads as one thing moving rather than three things blinking. */
.lp-how__step .lp-how__n { animation: lp-node 5.4s var(--ease-in-out) infinite; }
.lp-how__step:nth-child(2) .lp-how__n { animation-delay: 1.35s; }
.lp-how__step:nth-child(3) .lp-how__n { animation-delay: 2.7s; }
@keyframes lp-node {
  0%, 12%, 40%, 100% { transform: scale(1);    box-shadow: 0 0 0 0 color-mix(in srgb, var(--glow-accent) 40%, transparent); }
  22%                { transform: scale(1.16); box-shadow: 0 0 0 7px color-mix(in srgb, var(--glow-accent) 16%, transparent),
                                                            0 0 18px color-mix(in srgb, var(--glow-accent) 70%, transparent); }
}
.lp-how__n {
  flex-shrink: 0; width: 34px; height: 34px; border-radius: var(--radius-pill);
  display: flex; align-items: center; justify-content: center; margin-top: 2px;
  /* B3: every number on the page is mono — prices, counts, XP, steps. A
     figure set in the display face reads as a word; set in mono it reads as
     data, which is what a step number is. */
  font-family: var(--font-mono); font-weight: 700; font-size: 15px;
  color: var(--eng-space); background: var(--eng-green);
}
.lp-how__h { font-family: var(--font-display); font-weight: 700; font-size: 17px; margin: 2px 0 4px; color: var(--eng-white); }
.lp-how__p { font-size: 14.5px; color: rgba(255, 255, 255, 0.72); margin: 0; line-height: 1.6; }
.lp-how__lessons { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.lp-how__lesson { display: flex; gap: 10px; align-items: flex-start; font-size: 13.5px; color: rgba(255, 255, 255, 0.6); }
.lp-how__lesson-dot {
  flex-shrink: 0; width: 5px; height: 5px; border-radius: var(--radius-pill); margin-top: 8px;
  background: var(--eng-green);
  /* The three lessons are what the mechanism PRODUCES, so they light after the
     nodes do — offset past the signal's run, staggered among themselves. */
  animation: eng-blip 5.4s var(--ease-in-out) infinite;
}
.lp-how__lesson:nth-child(1) .lp-how__lesson-dot { animation-delay: 3.0s; }
.lp-how__lesson:nth-child(2) .lp-how__lesson-dot { animation-delay: 3.3s; }
.lp-how__lesson:nth-child(3) .lp-how__lesson-dot { animation-delay: 3.6s; }
/* the pass sets its own width (600px); this wrapper used to cap it at 480 and
   squeezed the stub against the gauges. */
.lp-how__report { max-width: 600px; margin: 40px auto 0; }
.lp-how__report-lead { font-size: 14px; color: rgba(255, 255, 255, 0.72); margin: 0 0 4px; text-align: center; }
.lp-how__report-lead a { color: var(--eng-green); text-decoration: none; }
.lp-how__report-note { font-size: 12.5px; color: rgba(255, 255, 255, 0.5); margin: 0 0 14px; text-align: center; }

/* ---- DAY (B-3: daily loop; <ol> — the loop IS sequential) ---- */
/* B4: #day IS a closed loop, so it is drawn as one — but as a RING OF FIVE
   PARTS, not a line chasing round a circle. The first version did exactly
   that and read as a loading spinner: thin arc, grey track, travelling dot.
   That says "waiting", which is the opposite of the section's claim, and it
   made the diagram cheap — a wireframe repeating the numbers already listed
   beside it.

   Now: five thick arcs with gaps (count them at a glance — that is the whole
   message, "there are only five"), and a core that draws each step as it
   happens. Ring says five-and-it-returns · core says what each step IS · the
   list says it in words. Three registers, none of them a duplicate of another.

   Cycle: 10s linear. One constant drives the segments, the badges, the core
   scenes and the list, so nothing can drift out of step with anything else. */
.lp-day { --day-cycle: 10s; }
.lp-day__grid {
  display: grid; grid-template-columns: 372px minmax(0, 1fr);
  gap: 44px; align-items: center;
  max-width: 920px; margin: 40px 0 0;
}
.lp-day__ring { width: 372px; height: 372px; display: block; overflow: visible; }

/* r=80 → circumference 502.65. 72deg = 100.53: 88 drawn, 12.53 gap. --k shifts
   ONE pattern round, so the geometry is written once and the five segments
   cannot disagree about their own size. Butt caps, not round: a round cap on a
   12px stroke adds 6px at each end and ate the gaps unevenly. */
.lp-day__seg, .lp-day__segfill {
  fill: none; stroke-linecap: butt;
  stroke-dasharray: 88 414.65;
  stroke-dashoffset: calc(var(--k) * -100.53px);
}
.lp-day__seg { stroke-width: 12; stroke: rgba(255, 255, 255, 0.085); }
.lp-day__segfill {
  stroke-width: 4; stroke: var(--glow-accent); opacity: 0;
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--glow-accent) 50%, transparent));
  animation: lp-dayseg var(--day-cycle) linear infinite;
}
@keyframes lp-dayseg {
  0%, 2%    { opacity: 0; }
  8%, 15%   { opacity: 1; }
  20%, 100% { opacity: 0; }
}
/* Segment 1 centred on 12 o'clock, where badge 1 sits: the dash starts at
   3 o'clock and spans 63deg, so its centre is at 31.5deg. */
.lp-day__ringwrap { transform-box: view-box; transform-origin: 120px 120px; rotate: -121.5deg; }

.lp-day__stdot {
  fill: var(--eng-space);
  stroke: color-mix(in srgb, var(--eng-green) 40%, transparent); stroke-width: 1.5;
  animation: lp-daystdot var(--day-cycle) linear infinite;
}
.lp-day__stnum {
  font-family: var(--font-mono); font-size: 14px; font-weight: 700;
  fill: var(--eng-green); text-anchor: middle; dominant-baseline: central;
  animation: lp-daystnum var(--day-cycle) linear infinite;
}
@keyframes lp-daystdot {
  0%, 2%, 20%, 100% { fill: var(--eng-space); stroke: color-mix(in srgb, var(--eng-green) 40%, transparent); }
  8%, 15%           { fill: var(--glow-accent); stroke: var(--glow-accent); }
}
@keyframes lp-daystnum {
  0%, 2%, 20%, 100% { fill: var(--eng-green); }
  8%, 15%           { fill: var(--eng-space); }
}

/* ---- the core: five scenes, one per segment ---------------------------- */
.lp-day__scene { opacity: 0; animation: lp-dayscene var(--day-cycle) linear infinite; }
@keyframes lp-dayscene {
  0%, 1%    { opacity: 0; }
  4%, 17%   { opacity: 1; }
  20%, 100% { opacity: 0; }
}
.lp-day__ring .d-panel { fill: rgba(255, 255, 255, 0.05); stroke: rgba(255, 255, 255, 0.12); stroke-width: 1.5; }
.lp-day__ring .d-bar   { fill: rgba(255, 255, 255, 0.22); }
.lp-day__ring .d-lit   { fill: var(--eng-purple-soft); }
.lp-day__ring .d-onlit { fill: color-mix(in srgb, var(--eng-space) 55%, transparent); }
.lp-day__ring .d-ready { fill: var(--eng-green); }
.lp-day__ring .d-go    { fill: none; stroke: var(--eng-space); stroke-width: 2.5;
                         stroke-linecap: round; stroke-linejoin: round; }
.lp-day__ring .d-wave  { fill: var(--eng-purple-soft); transform-box: fill-box; transform-origin: center;
                         animation: lp-daywave 1.1s var(--ease-in-out) infinite; animation-delay: var(--d, 0s); }
@keyframes lp-daywave { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } }
.lp-day__ring .d-aim    { fill: none; stroke: var(--glow-accent); stroke-width: 2; }
.lp-day__ring .d-aim-in { fill: var(--glow-accent); }
.lp-day__ring .d-check  { fill: none; stroke: var(--eng-green); stroke-width: 5;
                          stroke-linecap: round; stroke-linejoin: round; }
.lp-day__ring .d-loop, .lp-day__ring .d-loop-tip {
  fill: none; stroke: var(--glow-accent); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round;
}

.lp-day__loop { counter-reset: day; list-style: none; margin: 0; padding: 0; text-align: left; display: flex; flex-direction: column; gap: 6px; }
/* The rows carried no weight beside a ring that now has some, so the column
   read as text floating next to a graphic. Each step is a row that lights with
   its own segment — which also makes the ring↔list pairing visible rather than
   something the reader has to notice from two numbers agreeing. */
.lp-day__step {
  counter-increment: day; display: flex; gap: 14px; align-items: flex-start;
  font-size: 15px; color: rgba(255, 255, 255, 0.82); line-height: 1.55;
  padding: 11px 14px; border-radius: var(--radius-md);
  border: 1px solid transparent;
  animation: lp-dayrow var(--day-cycle) linear infinite;
}
@keyframes lp-dayrow {
  0%, 2%, 20%, 100% { background: transparent; border-color: transparent; }
  8%, 15%           { background: rgba(255, 255, 255, 0.045);
                      border-color: rgba(255, 255, 255, 0.08); }
}
.lp-day__step::before {
  content: counter(day); flex-shrink: 0; width: 26px; height: 26px; border-radius: var(--radius-pill);
  display: flex; align-items: center; justify-content: center; margin-top: 1px;
  font-family: var(--font-mono); font-size: 12px; font-weight: 700; color: var(--eng-green);
  background: color-mix(in srgb, var(--eng-green) 16%, transparent);
  animation: lp-daystep var(--day-cycle) linear infinite;
}
@keyframes lp-daystep {
  0%, 2%, 20%, 100% { color: var(--eng-green);
                      background: color-mix(in srgb, var(--eng-green) 16%, transparent);
                      box-shadow: none; }
  8%, 15%           { color: var(--eng-space);
                      background: var(--glow-accent);
                      box-shadow: 0 0 14px color-mix(in srgb, var(--glow-accent) 70%, transparent); }
}
/* One delay ladder, applied to the ring station and the list badge together —
   station N and step N are the same moment by construction. */
.lp-day__st--1 .lp-day__stdot, .lp-day__st--1 .lp-day__stnum, .lp-day__segfill--1, .lp-day__scene--1, .lp-day__step:nth-child(1), .lp-day__step:nth-child(1)::before { animation-delay: 0s; }
.lp-day__st--2 .lp-day__stdot, .lp-day__st--2 .lp-day__stnum, .lp-day__segfill--2, .lp-day__scene--2, .lp-day__step:nth-child(2), .lp-day__step:nth-child(2)::before { animation-delay: calc(var(--day-cycle) * 0.2); }
.lp-day__st--3 .lp-day__stdot, .lp-day__st--3 .lp-day__stnum, .lp-day__segfill--3, .lp-day__scene--3, .lp-day__step:nth-child(3), .lp-day__step:nth-child(3)::before { animation-delay: calc(var(--day-cycle) * 0.4); }
.lp-day__st--4 .lp-day__stdot, .lp-day__st--4 .lp-day__stnum, .lp-day__segfill--4, .lp-day__scene--4, .lp-day__step:nth-child(4), .lp-day__step:nth-child(4)::before { animation-delay: calc(var(--day-cycle) * 0.6); }
.lp-day__st--5 .lp-day__stdot, .lp-day__st--5 .lp-day__stnum, .lp-day__segfill--5, .lp-day__scene--5, .lp-day__step:nth-child(5), .lp-day__step:nth-child(5)::before { animation-delay: calc(var(--day-cycle) * 0.8); }

.lp-day__foot { margin: 34px 0 0; max-width: 920px; font-size: 14px; color: rgba(255, 255, 255, 0.55); }

@media (max-width: 799px) {
  .lp-day__grid { grid-template-columns: 1fr; gap: 24px; justify-items: center; }
  .lp-day__ring { width: 260px; height: 260px; }
  .lp-day__loop { width: 100%; max-width: 460px; }
  .lp-day__foot { text-align: left; }
}

/* ---- S4 CEFR REPORT SAMPLE (A-3.5; light preview panel on dark section) ----
   Mirrors the real placement report (report.ejs): white document panel, purple
   skill bars on a --eng-page track, yellow weakness sticker. The "ตัวอย่าง"
   ribbon + the section lead keep it from reading as the visitor's own result.
   All tokens / brand-channel rgba / color-mix — zero new hex. */
.lp-report .lp-sec__lead b { color: var(--eng-white); font-weight: 700; }
/* The card is a BOARDING PASS (Jeff, 2026-09-02): the ticket the five craft
   above are boarded with. Card stock stays LIGHT — a pass is paper, and it also
   keeps every value on it easy to measure against a white ground. */
.lp-report__panel {
  position: relative;
  max-width: 600px;
  margin: 28px auto 0;
  background: var(--eng-white);
  color: var(--eng-ink);
  border: 2px solid var(--eng-black);
  border-radius: var(--radius-xl);
  padding: 0 0 24px;
  box-shadow: var(--shadow-sticker-lg);
}
/* one pass of foil light, once, when the card arrives.
   The sheen clips ITSELF rather than the card clipping it: overflow:hidden on
   the panel also ate the stamp hanging off the top edge and the corners of the
   yellow stub. One element that owns its own window costs nothing. */
.lp-report__sheen {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  border-radius: calc(var(--radius-xl) - 2px);
}
.lp-report__sheen::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, 0.72) 50%, transparent 62%);
  transform: translateX(-120%);
}
.lp-reveal:not(.is-out) .lp-report__sheen::before { animation: lp-pass-sheen 1500ms var(--ease-out) 520ms both; }
@keyframes lp-pass-sheen { to { transform: translateX(120%); } }

/* the header strip a pass carries above the perforation */
.lp-report__strip {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 14px 22px;
  border-bottom: 2px dashed color-mix(in srgb, var(--eng-ink) 20%, transparent);
  border-radius: calc(var(--radius-xl) - 2px) calc(var(--radius-xl) - 2px) 0 0;
  background: color-mix(in srgb, var(--eng-purple) 7%, var(--eng-white));
}
.lp-report__brand {
  font-family: var(--font-display); font-weight: 800;
  font-size: 14px; letter-spacing: 0.14em; color: var(--eng-ink);
}

.lp-report__body { display: grid; grid-template-columns: 1fr auto 152px; }
.lp-report__main { padding: 20px 22px 22px; min-width: 0; }

/* ผู้โดยสาร → ปลายทาง, the way a pass states a leg */
.lp-report__route {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px;
  padding-bottom: 18px; margin-bottom: 18px;
  border-bottom: 1px solid color-mix(in srgb, var(--eng-ink) 12%, transparent);
}
.lp-report__field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.lp-report__field--to { text-align: right; }
.lp-report__flabel { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.12em; color: var(--eng-slate); }
.lp-report__fval { font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--eng-ink); }
/* the leg between them: a dotted run with a craft-shaped node riding it */
.lp-report__leg {
  position: relative; height: 2px;
  background: repeating-linear-gradient(90deg,
    color-mix(in srgb, var(--eng-ink) 28%, transparent) 0 4px, transparent 4px 9px);
}
.lp-report__leg::after {
  content: ""; position: absolute; top: 50%; left: 0;
  width: 7px; height: 7px; margin-top: -3.5px;
  border-radius: 50%;
  background: var(--eng-purple);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--eng-purple) 22%, transparent);
  animation: lp-pass-leg 5.5s var(--ease-in-out) infinite;
}
@keyframes lp-pass-leg { 0%, 100% { left: 0; } 50% { left: calc(100% - 7px); } }
.lp-report__ribbon {
  position: absolute; top: 52px; right: 22px;
  background: var(--eng-purple); color: var(--eng-white);
  border: 2px solid var(--eng-black); border-radius: var(--radius-pill);
  padding: 5px 16px; font-family: var(--font-body-en); font-weight: 800;
  font-size: 12px; letter-spacing: 0.08em; transform: rotate(-8deg);
  box-shadow: var(--shadow-sticker-sm);
  z-index: 2;                              /* above the sheen and the strip */
  /* 🚨 The armed state scales this to 1.9x, and an element at opacity 0 STILL
     counts for page overflow — scaling about the centre pushed its box 1px past
     the viewport at 390 and the walk caught it. Anchoring the growth to the
     right edge means the stamp can never cross the edge it sits against, at any
     width; a magic offset would only have moved the collision. It is also
     better ink physics: the stamp lands, and the ink spreads inward. */
  transform-origin: right center;
}
/* the tear line. Two notches bitten out of the edges are what make a rectangle
   read as a ticket; without them a dashed line is just a dashed line. */
.lp-report__perf {
  position: relative; width: 0;
  border-left: 2px dashed color-mix(in srgb, var(--eng-ink) 22%, transparent);
  margin: 14px 0;
}
.lp-report__notch {
  position: absolute; left: -10px; width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--eng-space);
  border: 2px solid var(--eng-black);
}
.lp-report__notch--top { top: -23px; }
.lp-report__notch--bot { bottom: -23px; }
/* the stub's block sits centred in its half. A pass stub IS mostly blank card
   — the empty space is the ticket, not a layout hole. */
.lp-report__stub {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 26px 14px 22px; text-align: center;
}
/* the machine-readable half. Deterministic bars — it encodes nothing, and it
   says nothing, which is why it carries no digits under it. */
.lp-report__code {
  margin-top: 14px; width: 100%; height: 42px;
  background: repeating-linear-gradient(90deg,
    var(--eng-ink) 0 2px, transparent 2px 4px,
    var(--eng-ink) 4px 5px, transparent 5px 9px,
    var(--eng-ink) 9px 12px, transparent 12px 14px);
  opacity: .78;
}
.lp-report__level { text-align: right; }
.lp-report__level-label { display: block; font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; color: var(--eng-slate); }
.lp-report__level-val { font-family: var(--font-display); font-weight: 800; font-size: 46px; color: var(--eng-purple); line-height: 1; }
.lp-report__level-conf { display: block; font-size: 11px; color: var(--eng-slate); }
.lp-report__h3 { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--eng-ink); margin: 0 0 14px; }
.lp-report__bars { display: flex; flex-direction: column; gap: 14px; }
/* a checkpoint node per skill, on a route line down the left of the readings */
.lp-report__bar { position: relative; padding-left: 20px; }
.lp-report__bar::before {
  content: ""; position: absolute; left: 4px; top: 4px; bottom: -14px; width: 1px;
  background: color-mix(in srgb, var(--eng-ink) 14%, transparent);
}
.lp-report__bar:last-child::before { bottom: auto; height: 8px; }
.lp-report__node {
  position: absolute; left: 0; top: 3px;
  width: 9px; height: 9px; border-radius: 50%;
  border: 2px solid var(--eng-purple);
  background: var(--eng-white);
  transition: background 320ms var(--ease-out), box-shadow 320ms var(--ease-out);
}
/* the node lights as its own gauge lands — the reading arrives, the checkpoint
   is passed. Same stagger as the fill below it, so they cannot drift apart. */
.lp-reveal:not(.is-out) .lp-report__node { background: var(--eng-purple); box-shadow: 0 0 0 3px color-mix(in srgb, var(--eng-purple) 18%, transparent); }
.lp-reveal.is-out .lp-report__node { background: var(--eng-white); box-shadow: none; transition: none; }
.lp-report__bar-row { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 5px; }
.lp-report__bar-name { font-size: 13.5px; font-weight: 700; color: var(--eng-ink); text-transform: capitalize; }
.lp-report__bar-val { font-size: 13.5px; color: var(--eng-slate); }
.lp-report__track { height: 10px; border-radius: var(--radius-pill); background: var(--eng-page); overflow: hidden; }
.lp-report__fill { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--eng-purple); }

/* B4: the bars DRAW rather than appear. A skill profile that is already full
   when you reach it is a picture; one that fills is a measurement. The width
   is the real sample value (inline style); this only animates the reveal of
   it, so nothing here can misreport a number. */
/* Driven by the REVEAL, not by page load. As a load-time animation the bars
   had already finished by the time anyone scrolled 3,000px down to them — the
   draw existed and no reader ever saw it. Armed only while .is-out, which the
   observer removes on arrival, so above-the-fold and JS-off both render full. */
.lp-report__fill { transform-origin: left center; transition: transform 1.4s var(--ease-out); }
.lp-reveal.is-out .lp-report__fill { transform: scaleX(0); transition: none; }
.lp-report__bar:nth-child(1) .lp-report__fill, .lp-report__bar:nth-child(1) .lp-report__node { transition-delay: .05s; }
.lp-report__bar:nth-child(2) .lp-report__fill, .lp-report__bar:nth-child(2) .lp-report__node { transition-delay: .18s; }
.lp-report__bar:nth-child(3) .lp-report__fill, .lp-report__bar:nth-child(3) .lp-report__node { transition-delay: .31s; }
.lp-report__bar:nth-child(4) .lp-report__fill, .lp-report__bar:nth-child(4) .lp-report__node { transition-delay: .44s; }
.lp-report__bar:nth-child(5) .lp-report__fill, .lp-report__bar:nth-child(5) .lp-report__node { transition-delay: .57s; }

/* The CEFR letter is the report's one loud number. */
.lp-report__level-val { animation: eng-glow-pulse 3.4s ease-in-out infinite; }
/* The "ตัวอย่าง" ribbon settles into place instead of being printed there. */
/* the ribbon is now the STAMP a gate agent thumps onto the pass: it comes in
   big and straight and settles small and crooked, the way ink lands. */
.lp-report__ribbon { transition: transform 520ms var(--ease-spring), opacity 300ms var(--ease-out); transition-delay: .55s; }
.lp-reveal.is-out .lp-report__ribbon { transform: scale(1.9) rotate(2deg); opacity: 0; transition: none; }
.lp-report__weak {
  margin: 18px 22px 0; background: var(--eng-yellow); border: 2px solid var(--eng-black);
  border-radius: var(--radius-lg); padding: 16px 18px; transform: rotate(-1deg);
}
.lp-report__h3--weak { margin-bottom: 8px; }
.lp-report__weak-list { margin: 0; padding-left: 18px; }
.lp-report__weak-list li { font-size: 13.5px; color: var(--eng-ink); line-height: 1.7; }

/* ---- CTA BAND --------------------------------------------- */
.lp-cta { text-align: center; padding: clamp(72px, 12vw, 120px) clamp(20px, 5vw, 48px); }
.lp-cta__h { font-size: clamp(34px, 7vw, 60px); line-height: 1.05; margin: 0 0 18px; }
.lp-cta__p { font-size: clamp(15px, 2.5vw, 17px); color: rgba(255, 255, 255, 0.7); margin: 0 0 36px; }
.lp-cta__btn {
  display: inline-flex; align-items: center; gap: 10px; background: var(--eng-green); color: var(--eng-black);
  font-family: var(--font-display); font-weight: 700; font-size: 18px; padding: 18px 40px;
  border-radius: var(--radius-pill); border: 2px solid var(--eng-black); text-decoration: none;
  box-shadow: 0 0 44px rgba(57, 255, 137, 0.4);
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
/* B4: the page's last loud control breathes, and a radar sweep turns behind
   the band. This is the ONE breathe on the section — MOTION-LANGUAGE §2. */
.lp-cta__btn { --glow-accent: var(--eng-green); animation: eng-breathe 3.2s ease-in-out infinite; }
.lp-cta__btn svg { animation: lp-nudge 2.6s var(--ease-in-out) infinite; }
.lp-cta { position: relative; overflow: hidden; }
.lp-cta::before {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: 780px; height: 780px; margin: -390px 0 0 -390px;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(from 0deg,
    transparent 0deg,
    color-mix(in srgb, var(--eng-green) 9%, transparent) 34deg,
    transparent 68deg);
  animation: eng-spin-slow 14s linear infinite;
}
.lp-cta > * { position: relative; }

.lp-cta__btn:hover { transform: translateY(-2px); box-shadow: 0 0 60px rgba(57, 255, 137, 0.6); }
.lp-cta__fine { font-size: 13px; color: rgba(255, 255, 255, 0.58); margin-top: 18px; } /* audit:contrast */

/* ---- FOOTER ----------------------------------------------- */
.lp-footer { border-top: 1px solid rgba(255, 255, 255, 0.08); padding: 36px clamp(20px, 5vw, 48px); }
.lp-footer__inner { max-width: 1240px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; }
.lp-footer__copy { font-size: 13px; color: rgba(255, 255, 255, 0.58); } /* audit:contrast */
/* FIX-2: company registration + LINE OA moved here from the deleted #trust. */
.lp-footer__company { font-size: 12.5px; line-height: 1.7; color: rgba(255, 255, 255, 0.55); margin-top: 8px; max-width: 420px; } /* audit:contrast */
.lp-footer__company a { color: var(--eng-green); text-decoration: none; }
.lp-footer__company a:hover { text-decoration: underline; }
.lp-footer__links { display: flex; gap: 26px; flex-wrap: wrap; }
.lp-footer__links a { font-size: 13px; color: rgba(255, 255, 255, 0.55); text-decoration: none; transition: color var(--dur-fast) var(--ease-out); }
.lp-footer__links a:hover { color: var(--eng-white); }
/* M2-B: legal links line under the footer inner row */
.lp-footer__legal { max-width: 1240px; margin: 14px auto 0; font-size: 12px; color: rgba(255, 255, 255, 0.55); } /* audit:contrast */
.lp-footer__legal a { color: rgba(255, 255, 255, 0.65); text-decoration: underline; text-underline-offset: 3px; transition: color var(--dur-fast) var(--ease-out); }
.lp-footer__legal a:hover { color: var(--eng-white); }

/* ---- A-1: mobile nav toggle + sticky CTA + reduced-motion --- */
.lp-nav__toggle { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }
.lp-nav__burger { display: none; }
.lp-sticky-cta { display: none; }


/* ---- RESPONSIVE ------------------------------------------- */
@media (max-width: 900px) {
  .lp-hero__grid { grid-template-columns: 1fr; }
  .lp-sat-wrap { display: none; }
  /* Saturn beside five lanes costs ~60px of a 358px column — the payoff lines
     then wrap to four. Put the destination under the stack instead. */
  .lp-fleet { grid-template-columns: 1fr; padding-bottom: 0; }
  .lp-fleet__dest { margin-top: 2px; }
  .lp-lane { grid-template-columns: 1fr; gap: 4px; }
  /* the craft sits at left:100% with HALF its own width hanging past the track.
     On desktop the gap column absorbs that; at 390 the track is the full width
     and that half became horizontal page overflow, so the track gives it back
     as a right margin — 36px, half of the 68px the craft can reach. */
  /* The tilt aims each lane at a destination on the RIGHT. Stacked, the planet
     is below, so the same tilt would point at nothing — five slanted rails with
     no target read as an accident. Flat, and let the planet sit under them. */
  .lp-lane__track { height: 46px; margin-right: 36px; rotate: 0deg; }
  /* an ellipse with three stations does not survive a 358px column — the
     stations become a plain row above the panel and the ring is dropped */
  /* Three 48-character route titles across a 358px row is 3 columns of ~110px.
     Stack them: the console is still a console, the switches are just in a
     column the way they would be down the side of a real panel. */
  .lp-console.is-armed .lp-console__routes { grid-template-columns: 1fr; gap: 8px; }
  .lp-console__bezel { padding: 16px 14px; border-radius: 14px; }
  .lp-console__corner { width: 12px; height: 12px; }
  .lp-console__status { display: none; }   /* the row is for the title alone */
  .lp-lane__track::before { right: 0; }
  .lp-usp { max-width: none; }

  /* B-2/M2 — mobile menu (checkbox-hack; the desktop links become a dropdown) */
  .lp-nav__burger {
    display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; font-size: 22px; line-height: 1; cursor: pointer;
    color: var(--eng-white); background: rgba(255, 255, 255, 0.06);
    border: 1.5px solid rgba(255, 255, 255, 0.18); border-radius: 12px;
  }
  .lp-nav__links {
    display: none; position: absolute; top: 100%;
    left: clamp(20px, 5vw, 48px); right: clamp(20px, 5vw, 48px);
    flex-direction: column; align-items: stretch; gap: 4px;
    background: var(--eng-space); border: 1.5px solid rgba(255, 255, 255, 0.14);
    border-radius: 16px; padding: 12px; margin-top: 10px; z-index: 5;
  }
  .lp-nav__toggle:checked ~ .lp-nav__links { display: flex; }
  .lp-nav__link { padding: 12px 14px; font-size: 16px; border-radius: 10px; }
  .lp-nav__link:hover { background: rgba(255, 255, 255, 0.06); }

  /* B-3 — mobile sticky CTA. Hidden by default; the scroll observer reveals it
     once the hero (with the demo) is scrolled past (A-3 D2 — see below). */
  .lp-sticky-cta {
    display: none; align-items: center; justify-content: center; gap: 8px;
    position: fixed; left: 12px; right: 12px; bottom: 12px; z-index: 40;
    background: var(--eng-green); color: var(--eng-black);
    font-family: var(--font-display); font-weight: 700; font-size: 16px;
    padding: 15px; border-radius: var(--radius-pill); border: 2px solid var(--eng-black);
    text-decoration: none; box-shadow: 0 6px 28px rgba(0, 0, 0, 0.4);
  }
}
/* M5 — while the consent banner is up (fixed, bottom), suppress the sticky CTA
   so the two never overlap. :has() is already used at the top of this file. */
body:has(#consent-banner:not([hidden])) .lp-sticky-cta { display: none; }

/* A-3 (D2) — restore the sticky CTA on mobile, scroll-coordinated: it reveals
   only once the hero (with the playable demo) is fully scrolled past, so it never
   covers the demo's interactive area (gate hint · chips · zones · Next).
   public/js/landing-sticky.js toggles body.is-past-hero. The consent-banner
   suppression (M5, id-specificity) still wins over this, so the two never stack. */
@media (max-width: 900px) {
  body.is-past-hero .lp-sticky-cta { display: inline-flex; }
  /* A-4b — hide the sticky once the closing CTA band (#placement) / footer is in
     view: the footer holds the company + legal trust content (the sticky must not
     cover it), and it is redundant past the S7 CTA. Equal specificity to the rule
     above → wins by source order when both classes are set (landing-sticky.js). */
  body.is-at-end .lp-sticky-cta { display: none; }
}

/* Live chat launcher (2026-09-05) — the landing mounts the shared chat widget
   (see the include at the page tail). On MOBILE it joins the sticky CTA's
   scroll choreography for the same D2 reason the sticky has one: nothing may
   float over the playable demo's interactives (chips · zones · Next) while
   the hero is on screen. Desktop (≥900px) shows the launcher always — the
   hero grid leaves the bottom-right corner clear (verified at the walk). */
@media (max-width: 900px) {
  /* Hidden through the hero; .lp-body scopes this to the landing so the
     global .cht default (components.css, bottom:18px) is untouched elsewhere. */
  .lp-body .cht { display: none; }
  body.is-past-hero .cht { display: block; } /* (0,2,1) beats the hide (0,2,0) */
  /* [hidden] guard (the hidden-display-guard law — it caught this rule's
     first draft): the chat root ships hidden until chat-widget.js unhides
     it. If that script fails while landing-sticky.js still sets
     is-past-hero, the reveal above would force an empty dead shell visible —
     (0,2,1) beats preflight's [hidden] (0,1,0). This guard at (0,3,0)
     outranks the reveal, so hidden stays hidden no matter what runs. */
  .lp-body .cht[hidden] { display: none; }
  /* When the sticky bar is ACTUALLY shown, lift the launcher above it. This
     predicate MUST mirror the sticky's visibility exactly (the three rules
     above + the M5 consent suppression): past-hero AND NOT at-end AND NOT
     consent-banner-open. Any other moment the launcher keeps bottom:18px —
     a bar that is not on screen must not leave a ghost offset. */
  body.is-past-hero:not(.is-at-end):not(.cht-open):not(:has(#consent-banner:not([hidden]))) .cht {
    bottom: 78px; /* 12 sticky bottom + ~54 bar + 12 gap — measured at the walk */
  }
  /* While the chat sheet is open it owns the bottom of the screen: the
     sticky CTA would sit on top of it (Jeff's iPhone shot, 2026-09-18), and
     the lift above is excluded by :not(.cht-open) so the sheet reaches the
     real bottom edge. */
  body.cht-open .lp-sticky-cta { display: none; }
}

/* ── PICKER — 3 pain-point doors (front door pivot 2026-07-28) ───────────
   Mobile-first stacked doors → 3-col ≥720px. Colours are tokens / white +
   green channels only (zero new hex); the door tint mirrors the hero demo
   card; hover/focus use the green accent. Pure CSS (the doors are <a> links). */
.lp-picker__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
  margin-top: 28px;
}
@media (min-width: 720px) {
  .lp-picker__grid { grid-template-columns: repeat(3, 1fr); }
}
.lp-picker__door {
  display: flex;
  flex-direction: column;
  gap: 8px;
  text-align: left;
  padding: 20px 20px 18px;
  border-radius: 18px;
  border: 1.5px solid rgba(255, 255, 255, 0.12);
  background: color-mix(in srgb, var(--eng-white) 5%, transparent);
  color: var(--eng-white);
  text-decoration: none;
  transition: border-color 0.2s ease, transform 0.2s ease, background 0.2s ease;
}
/* The door carries .eng-lift for the transform + glow (the ONE hover rule,
   MOTION-LANGUAGE §3); this rule keeps only what is specific to a door.

   Its accent is GREEN even though #picker's section accent is purple, and
   that is deliberate rather than an exception: a door is an ACTION ("ลองบท
   แรกฟรี →") and green is the action channel across the whole product, while
   the purple eyebrow labels what the SECTION is about. Purple surface, green
   control is the house pattern, not two accents fighting. */
.lp-picker__door { --glow-accent: var(--eng-green); position: relative; overflow: hidden; }
/* B4: a light runs along each door's top edge, one after another, so the row
   reads as three live options rather than three boxes. Top edge only — a full
   border chase is a casino, and this section is a decision. */
.lp-picker__door::before {
  content: "";
  position: absolute; top: 0; left: 0; height: 1.5px; width: 45%;
  background: linear-gradient(to right, transparent, var(--eng-green), transparent);
  animation: lp-doorsweep 6s var(--ease-in-out) infinite;
  pointer-events: none;
}
.lp-picker__door::before { animation-delay: var(--phase, 0s); }
@keyframes lp-doorsweep {
  0%, 100% { transform: translateX(-110%); opacity: 0; }
  12%      { opacity: 1; }
  46%      { transform: translateX(255%); opacity: 1; }
  56%      { transform: translateX(255%); opacity: 0; }
}
/* The arrow leans toward the door it opens. */
.lp-picker__go svg { animation: lp-nudge 2.6s var(--ease-in-out) infinite; }
.lp-picker__door:nth-child(2) .lp-picker__go svg { animation-delay: .3s; }
.lp-picker__door:nth-child(3) .lp-picker__go svg { animation-delay: .6s; }
@keyframes lp-nudge {
  0%, 62%, 100% { transform: translateX(0); }
  76%           { transform: translateX(4px); }
}
.lp-picker__door:hover,
.lp-picker__door:focus-visible {
  border-color: var(--eng-green);
  background: color-mix(in srgb, var(--eng-white) 8%, transparent);
  outline: none;
}
.lp-picker__q {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.12rem;
  line-height: 1.35;
  color: var(--eng-white);
}
.lp-picker__sub {
  font-size: 0.95rem;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.72);
}
.lp-picker__go {
  margin-top: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--eng-green);
}
.lp-picker__go svg { width: 1em; height: 1em; }
/* ---- PICKER · a station per door (2026-09-01) ---------------------------
   The doors are the page's decision point and, once #inside and #how had
   panels, they were the least drawn thing on it: three identical grey boxes,
   3 animating elements in the whole section, and an h2 that highlighted no
   word while every other section highlights one.

   SVG rather than a generated image, and the subject decided that, not the
   price: these three doors are FEELINGS, and an illustration of a feeling
   lands on a worried stock face — the exact register 10/10 real users
   rejected — while a drawing also cannot move, and every other section here
   now moves in one grammar.

   Each station shares its door's 6s clock and 0/2/4s offsets, so a door's
   edge light and its station belong to the same door. Only transform and
   opacity animate. */
.lp-station { display: block; width: 100%; height: 86px; margin: 2px 0 10px; overflow: visible; }
/* The phase comes from the DOOR, not from the station, so the edge light and
   the station can never drift apart — reorder DOORS and both follow. The
   earlier draft tried to make the delay inherit from the door, which that
   property does not do — so all three stations ran in lockstep and the row
   read as one twitch instead of three options taking turns.
   (Worded around the literal declaration on purpose: a pin scans this file
   for it, and a comment quoting the thing it warns about re-trips the scan —
   which is exactly what happened on the first draft of that pin.) */
.lp-picker__door:nth-child(2) { --phase: 2s; }
.lp-picker__door:nth-child(3) { --phase: 4s; }

/* shared parts */
.lp-st-bubble { fill: none; stroke: var(--eng-white); stroke-width: 1.5; opacity: .16; }
.lp-st-line   { fill: var(--eng-white); opacity: .2; }
.lp-st-chip   { fill: var(--eng-white); opacity: .18; }

/* 1 · business — the missing phrase arrives ------------------------------- */
.lp-st1-slot { fill: none; stroke: var(--eng-white); stroke-width: 1.5; stroke-dasharray: 4 4; opacity: .3;
               animation: lp-st1-slot 6s var(--ease-in-out) infinite; animation-delay: var(--phase, 0s); }
.lp-st1-word { fill: var(--glow-accent); opacity: 0;
               animation: lp-st1-word 6s var(--ease-spring) infinite; animation-delay: var(--phase, 0s); }
.lp-st1-yours { animation: lp-st1-lit 6s var(--ease-out) infinite; animation-delay: var(--phase, 0s); }
@keyframes lp-st1-slot { 0%, 14% { opacity: .3; } 22%, 66% { opacity: 0; } 76%, 100% { opacity: .3; } }
@keyframes lp-st1-word {
  0%, 12%   { transform: translate(24px, -28px) scale(.85); opacity: 0; }
  26%, 64%  { transform: translate(0, 0) scale(1); opacity: 1; }
  74%, 100% { transform: translate(24px, -28px) scale(.85); opacity: 0; }
}
@keyframes lp-st1-lit { 0%, 18% { opacity: .16; } 28%, 64% { opacity: .5; } 74%, 100% { opacity: .16; } }

/* 2 · everyday — the gap in what you already know closes ------------------ */
.lp-st2-bank { fill: var(--eng-white); opacity: .22;
               animation: lp-st2-b 6s var(--ease-out) infinite; animation-delay: calc(var(--phase, 0s) + var(--d, 0s)); }
.lp-st2-b    { fill: var(--eng-white); opacity: .16;
               animation: lp-st2-b 6s var(--ease-out) infinite; animation-delay: calc(var(--phase, 0s) + var(--d, 0s)); }
.lp-st2-gap  { fill: none; stroke: var(--eng-white); stroke-width: 1.5; stroke-dasharray: 4 4; opacity: .32;
               animation: lp-st2-gap 6s var(--ease-in-out) infinite; animation-delay: var(--phase, 0s); }
.lp-st2-fill { fill: var(--glow-accent); opacity: 0;
               animation: lp-st2-fill 6s var(--ease-spring) infinite; animation-delay: var(--phase, 0s); }
@keyframes lp-st2-gap  { 0%, 12% { opacity: .32; } 22%, 68% { opacity: 0; } 78%, 100% { opacity: .32; } }
@keyframes lp-st2-fill {
  0%, 10%   { transform: translateY(-26px); opacity: 0; }
  24%, 66%  { transform: translateY(0); opacity: 1; }
  76%, 100% { transform: translateY(-26px); opacity: 0; }
}
@keyframes lp-st2-b { 0%, 26% { opacity: .16; } 36%, 62% { opacity: .42; } 72%, 100% { opacity: .16; } }

/* 3 · toeic — where to start, on the real 7-part map ---------------------- */
.lp-st3-lab   { fill: var(--eng-white); opacity: .78; font-family: var(--font-mono); font-size: 11px; font-weight: 700; }
.lp-st3-p     { fill: var(--eng-white); opacity: .16; }
.lp-st3-split { fill: var(--eng-white); opacity: .22; }
.lp-st3-here  { fill: var(--glow-accent); opacity: .1;
                animation: lp-st3-here 6s var(--ease-out) infinite; animation-delay: var(--phase, 0s); }
.lp-st3-pin   { fill: var(--glow-accent); opacity: 0;
                animation: lp-st3-pin 6s var(--ease-spring) infinite; animation-delay: var(--phase, 0s); }
.lp-st3-ring  { fill: none; stroke: var(--glow-accent); stroke-width: 1.5; opacity: 0;
                transform-box: fill-box; transform-origin: center;
                animation: lp-st3-ring 6s var(--ease-out) infinite; animation-delay: var(--phase, 0s); }
@keyframes lp-st3-here { 0%, 16% { opacity: .1; } 26%, 66% { opacity: 1; } 76%, 100% { opacity: .1; } }
@keyframes lp-st3-pin {
  0%, 8%    { transform: translateY(-16px); opacity: 0; }
  22%, 66%  { transform: translateY(0); opacity: 1; }
  76%, 100% { transform: translateY(-16px); opacity: 0; }
}
@keyframes lp-st3-ring {
  0%, 22%   { transform: scale(.86); opacity: 0; }
  32%       { opacity: .8; }
  48%, 100% { transform: scale(1.12); opacity: 0; }
}

.lp-picker__exit {
  margin-top: 24px;
  text-align: center;
  font-size: 0.95rem;
  color: rgba(255, 255, 255, 0.62);
}
.lp-picker__exit a {
  color: var(--eng-green);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.lp-picker__exit a:hover { color: var(--eng-white); }

/* =====================================================================
   REDUCED MOTION — DECLARED LAST, AND THAT POSITION IS LOAD-BEARING.
   These selectors tie the ones they override on specificity, so whichever
   comes later in the file wins. This block used to sit mid-file and every
   rule written after it — the whole #picker and #report set — kept animating
   for a reader who had asked for less motion. Nothing failed; it just
   quietly did not apply. components.css carries the same warning on its own
   block. Any NEW animation goes in here, and this block stays at the bottom.
   ===================================================================== */
/* ---- WHAT'S INSIDE (2026-09-01) -----------------------------------------
   The section that names the product. Four cards, and each icon is a picture
   of the number beside it: a fanning deck of lessons, three options with the
   right one lighting, a waveform, a reply arriving. Motion here is meaning —
   an icon that merely wiggles would be decoration, which is what §7 of
   MOTION-LANGUAGE forbids.

   Every animation below moves ONLY transform or opacity. `fill` is a paint
   property and is not on the allowed list, so the "correct option" is a second
   green bar sitting on top of the grey one and fading in — not a colour tween.

   No .eng-lift on the cards: they are not links, and a lift on something that
   cannot be clicked is a false affordance. */
.lp-inside__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 14px;
  grid-template-columns: 1fr;
}
@media (min-width: 560px) { .lp-inside__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 980px) { .lp-inside__grid { grid-template-columns: repeat(4, 1fr); } }

.lp-inside__card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 22px 20px 20px;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(255, 255, 255, 0.09);
  background: rgba(255, 255, 255, 0.028);
}
/* WCAG 1.4.11 wants >=3:1 for a graphic you must see to understand the card.
   --eng-purple on this card ground measures 2.99:1 — one hundredth under, and
   the contrast audit cannot say so because it samples TEXT. It applied to the
   waveform that shipped this morning as much as to the new reason-bars, so the
   fix is one token both read rather than a patch on the newer of the two.
   Uses the existing --eng-purple-soft token rather than a color-mix: a mix
   computes to color(srgb ...), which every rgb()-only parser — including the
   probe that measured this, on its first run — reads as black. A value that
   cannot be measured is worse than a plainer one that can.
   The quiet greys stay quiet on purpose — .lp-ico-opt and
   .lp-ico-wrong are context, and the meaning is carried by the lit option
   (14.36:1) and the miss mark (5.70:1) beside them. */
.lp-inside__ico { --ico-purple: var(--eng-purple-soft); }
.lp-inside__ico { width: 46px; height: 46px; margin-bottom: 13px; overflow: visible; }
.lp-inside__num {
  font-family: var(--font-mono);
  font-size: clamp(26px, 3.4vw, 32px);
  font-weight: 700;
  line-height: 1.05;
  color: var(--eng-white);
  font-variant-numeric: tabular-nums;
}
.lp-inside__label { font-size: 14.5px; font-weight: 700; color: rgba(255, 255, 255, 0.94); }
/* 0.68 not 0.6: this sub sits at 12.5px, two steps below .lp-sec__lead, and
   the audit grades small text harder. Measured at 0 violations. */
.lp-inside__sub { font-size: 12.5px; line-height: 1.5; color: rgba(255, 255, 255, 0.68); }

/* Phone: the stacked card is ~195px tall and four of them is a 780px scroll to
   read four numbers. Turned on its side — icon left, the three lines right —
   the same card is ~110px and the whole inventory fits in one thumb-flick.
   Grid areas, so no wrapper element and no second markup shape to keep in
   sync with the desktop one. */
@media (max-width: 559px) {
  .lp-inside__card {
    display: grid;
    grid-template-columns: 46px 1fr;
    grid-template-areas: "ico num" "ico label" "ico sub";
    column-gap: 15px;
    row-gap: 1px;
    align-items: start;
    padding: 18px 18px 17px;
  }
  .lp-inside__ico   { grid-area: ico; margin-bottom: 0; align-self: center; }
  .lp-inside__num   { grid-area: num; font-size: 25px; }
  .lp-inside__label { grid-area: label; }
  .lp-inside__sub   { grid-area: sub; }
}

/* -- icon 1 · a deck of lessons fanning ------------------------------------ */
.lp-ico-card { fill: rgba(255, 255, 255, 0.42); animation: lp-fan 4.6s var(--ease-in-out) infinite; animation-delay: var(--d, 0s); }
.lp-ico-card:last-of-type { fill: var(--eng-purple); }
@keyframes lp-fan { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(3px); } }

/* -- icon 2 · three options, the right one lighting ------------------------ */
.lp-ico-opt { fill: rgba(255, 255, 255, 0.32); }
.lp-ico-hit { fill: var(--eng-green); opacity: 0; animation: lp-pick 3.6s var(--ease-out) infinite; animation-delay: var(--d, 0s); }
@keyframes lp-pick { 0%, 6% { opacity: 0; } 14%, 26% { opacity: 1; } 36%, 100% { opacity: 0; } }

/* -- icon 3 · a waveform ---------------------------------------------------
   transform-box: fill-box is required, or transform-origin resolves against
   the SVG viewport and the bars scale away from their own centres. */
.lp-ico-wave {
  fill: var(--ico-purple);
  transform-box: fill-box;
  transform-origin: center;
  animation: lp-wave 1.5s var(--ease-in-out) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes lp-wave { 0%, 100% { transform: scaleY(0.32); } 50% { transform: scaleY(1); } }

/* -- icon 4 · the wrong answer, then the reason arriving under it ----------
   One 3.8s loop: the miss is marked, then two lines of explanation wipe in
   beneath it and hold. Same grammar as its three siblings (staggered by --d,
   paused off-screen with the rest of the section). */
.lp-ico-wrong { fill: rgba(255, 255, 255, 0.14); }
.lp-ico-x {
  stroke: var(--eng-red); stroke-width: 2; stroke-linecap: round; fill: none;
  animation: lp-ico-miss 3.8s var(--ease-out) infinite;
}
@keyframes lp-ico-miss {
  0%, 6%    { opacity: 0; }
  14%, 82%  { opacity: 1; }
  92%, 100% { opacity: 0; }
}
.lp-ico-why {
  fill: var(--ico-purple);
  transform-box: fill-box; transform-origin: left center;
  animation: lp-ico-why 3.8s var(--ease-out) infinite;
  animation-delay: var(--d, 0s);
}
@keyframes lp-ico-why {
  0%, 22%   { transform: scaleX(0); opacity: 0; }
  38%, 82%  { transform: scaleX(1); opacity: 1; }
  92%, 100% { transform: scaleX(0); opacity: 0; }
}

/* ---- HOW · the three panels that SHOW the mechanism (2026-09-01) ---------
   #how described the loop in words and drew nothing. Measured before this:
   7 elements animating in the whole section, all of them spine and node — the
   claim "ยิ่งพลาด ระบบยิ่งรู้จักคุณ" was asserted three times and demonstrated
   zero times.

   Every panel runs on the SPINE'S OWN 5.4s clock at the same 1.35s offsets the
   nodes already use, so the section reads as one mechanism running rather than
   three loops ticking beside each other. Break that and it becomes noise.

   Paint properties (fill, stroke) are never animated — only transform and
   opacity, per MOTION-LANGUAGE §6. Where a shape must appear to change colour
   it is drawn in the accent and revealed by opacity instead. */
.lp-how__step { flex-wrap: wrap; }
/* basis 0, not 320px: a 320px basis does not fit beside the 50px number
   column at 390, so the body wrapped and left the step number standing alone
   on its own line. Basis 0 lets it shrink to whatever is left and grow into
   whatever is spare — it never leaves the number's side. */
.lp-how__body { flex: 1 1 0; min-width: 0; }
.lp-how__demo { flex: 0 0 150px; width: 150px; height: 54px; align-self: flex-start; margin-top: 4px; overflow: visible; }
/* ≤560px: a pass held in one hand is a STACK, not two columns — the stub goes
   under the main half and the perforation turns with it. Two columns survived
   the width but squeezed the leg into the passenger's own name. */
@media (max-width: 560px) {
  .lp-report__body { grid-template-columns: 1fr; }
  .lp-report__main { padding: 18px 18px 20px; }
  .lp-report__leg { display: none; }        /* no room for a run between them */
  .lp-report__route { grid-template-columns: 1fr 1fr; gap: 14px; }
  .lp-report__perf {
    width: auto; height: 0; margin: 0 18px;
    border-left: 0;
    border-top: 2px dashed color-mix(in srgb, var(--eng-ink) 22%, transparent);
  }
  .lp-report__notch { top: -10px; bottom: auto; }
  .lp-report__notch--top { left: -28px; }
  .lp-report__notch--bot { left: auto; right: -28px; }
  .lp-report__stub {
    flex-direction: row; flex-wrap: wrap; justify-content: space-between;
    align-items: center; gap: 4px 14px; padding: 18px; text-align: left;
  }
  .lp-report__level-label { order: 1; flex-basis: 100%; }
  .lp-report__level-val   { order: 2; }
  .lp-report__level-conf  { order: 3; }
  .lp-report__code        { order: 4; width: 128px; height: 34px; margin-top: 0; margin-left: auto; }
  /* the stamp keeps the top-right corner, so the strip stacks and RESERVES that
     corner rather than the stamp landing on the words. A magic top/right offset
     would have moved the collision instead of removing it. */
  .lp-report__strip { flex-direction: column; align-items: flex-start; gap: 2px; padding: 12px 128px 12px 18px; }
  .lp-report__ribbon { top: 14px; right: 16px; }
}

@media (max-width: 760px) {
  /* Wrapped, the panel would start under the step number. Indent it to the
     body's own left edge so the diagram stays attached to the sentence it
     illustrates. */
  /* A 50px MARGIN on a full-width flex item overflows by exactly 50px; the
     same 50px as PADDING with border-box sits inside the line. The SVG then
     scales to the content box, and xMinYMid (set in the markup) keeps it left-
     aligned with the sentence instead of drifting to the middle. */
  .lp-how__demo {
    flex: 0 0 100%; width: 100%; box-sizing: border-box;
    padding-left: 50px; margin: 12px 0 2px;
  }
}

/* -- 1 · your answer is not scored, it becomes evidence -------------------- */
.lp-hd-opt { fill: var(--eng-white); opacity: .14; }
.lp-hd1-chosen { animation: lp-hd1-chose 5.4s var(--ease-in-out) infinite; }
.lp-hd1-ring   { fill: none; stroke: var(--glow-accent); stroke-width: 1.5; opacity: 0;
                 animation: lp-hd1-ring 5.4s var(--ease-in-out) infinite; }
.lp-hd1-dot    { fill: var(--glow-accent); opacity: 0;
                 animation: lp-hd1-dot 5.4s var(--ease-out) infinite; }
@keyframes lp-hd1-chose { 0%, 6% { opacity: .14; } 14%, 34% { opacity: .34; } 44%, 100% { opacity: .14; } }
@keyframes lp-hd1-ring  { 0%, 6% { opacity: 0; } 13%, 28% { opacity: 1; } 38%, 100% { opacity: 0; } }
@keyframes lp-hd1-dot {
  0%, 15%   { transform: translateX(0); opacity: 0; }
  22%       { opacity: 1; }
  36%       { transform: translateX(44px); opacity: 1; }
  46%, 100% { transform: translateX(44px); opacity: 0; }
}

/* -- 2 · a signal, deliberately NOT a verdict ------------------------------ */
.lp-hd-cell  { fill: var(--eng-white); opacity: .12; }
.lp-hd2-ring { fill: none; stroke: var(--glow-accent); stroke-width: 1.5; stroke-dasharray: 4 4; opacity: 0;
               animation: lp-hd2-ring 5.4s var(--ease-in-out) infinite; animation-delay: 1.35s; }
.lp-hd2-dot  { fill: var(--glow-accent); opacity: 0; transform-box: fill-box; transform-origin: center;
               animation: lp-hd2-dot 5.4s var(--ease-spring) infinite; animation-delay: calc(1.35s + var(--d, 0s)); }
@keyframes lp-hd2-ring { 0%, 9% { opacity: 0; } 18%, 62% { opacity: .95; } 72%, 100% { opacity: 0; } }
@keyframes lp-hd2-dot {
  0%, 15%   { opacity: 0; transform: scale(.3); }
  25%, 60%  { opacity: 1; transform: scale(1); }
  70%, 100% { opacity: 0; transform: scale(1); }
}

/* -- 3 · today's ten slots, then tomorrow's ------------------------------- */
/* Flat single classes throughout this panel, deliberately. The first cut used
   `.lp-hd3-row rect` for the plain slots, which forced every override on the
   lit ones to out-specify a descendant selector — and the reduced-motion block
   then lost twice in a row: once on opacity, once on animation, each found
   only by looking at the rendered page. Flat classes let source order decide,
   which is how every other override on this page already works. */
.lp-hd3-slot { fill: var(--eng-white); opacity: .12; }
.lp-hd3-miss  { fill: var(--glow-accent); opacity: .1;
                animation: lp-hd3-miss 5.4s var(--ease-in-out) infinite; animation-delay: 2.7s; }
.lp-hd3-carry { fill: var(--glow-accent); opacity: 0;
                animation: lp-hd3-carry 5.4s var(--ease-out) infinite; animation-delay: 2.7s; }
.lp-hd3-new   { fill: var(--glow-accent); opacity: .1;
                animation: lp-hd3-new 5.4s var(--ease-out) infinite; animation-delay: calc(2.7s + var(--d, 0s)); }
@keyframes lp-hd3-miss  { 0%, 7% { opacity: .1; } 15%, 36% { opacity: 1; } 46%, 100% { opacity: .1; } }
@keyframes lp-hd3-carry {
  0%, 20%   { transform: translate(0, 0); opacity: 0; }
  27%       { opacity: 1; }
  47%       { transform: translate(-84px, 4px); opacity: 1; }
  55%, 100% { transform: translate(-84px, 4px); opacity: 1; }
}
@keyframes lp-hd3-new   { 0%, 45% { opacity: .1; } 56%, 80% { opacity: 1; } 90%, 100% { opacity: .1; } }

/* -- the loop closes ------------------------------------------------------ */
.lp-how__loop {
  display: flex; align-items: center; gap: 10px;
  max-width: 720px; margin: 16px auto 0; padding-left: 50px;
  font-size: 13.5px; color: rgba(255, 255, 255, 0.62);
}
.lp-how__loop b { color: rgba(255, 255, 255, 0.9); font-weight: 700; }
.lp-how__loop-ico { flex: none; width: 22px; height: 22px; }
/* r=9 → circumference 56.5. A dash chasing its own circle reads as a cycle;
   a full spin reads as a spinner, which would say "loading", not "repeats". */
.lp-how__loop-arc   { fill: none; stroke: var(--glow-accent); stroke-width: 2; opacity: .3;
                      stroke-dasharray: 3 5; }
.lp-how__loop-chase { fill: none; stroke: var(--glow-accent); stroke-width: 2.5; stroke-linecap: round;
                      stroke-dasharray: 7 49.5; animation: lp-how-cycle 3.6s linear infinite; }
@keyframes lp-how-cycle { to { stroke-dashoffset: -56.5; } }

/* Sections nobody is looking at stop animating (landing-fleet.js adds the
   class). play-state PAUSES the timeline rather than resetting it, so a
   section resumes mid-loop instead of snapping back to frame one.

   `section.is-offscreen`, not `.is-offscreen`, and the extra element counts:
   the `animation:` SHORTHAND resets animation-play-state to running, so every
   `.lp-ico-card { animation: ... }` further down the file was quietly undoing
   this at equal specificity. Measured: #inside sat off screen with 12 loops
   still running. One element in the selector puts it beyond the reach of any
   single-class rule, wherever a future one lands. */
section.is-offscreen *,
section.is-offscreen *::before,
section.is-offscreen *::after { animation-play-state: paused; }

/* ── LIBRARY (UAT round 3 ②) — newest live articles before the CTA band.
   The landing is a DARK page end-to-end, so these are dark-glass cards; the
   category chip + left rail take the engine's hue via --lib-hue (rgb channels
   from the server; no hex minted here). */
.lp-library__sub { text-align: center; color: rgba(255, 255, 255, 0.65); margin: 6px 0 26px; }
.lp-library__grid { display: grid; gap: 16px; }
@media (min-width: 760px) { .lp-library__grid { grid-template-columns: repeat(3, 1fr); } }
.lp-libcard {
  display: flex; flex-direction: column; gap: 10px;
  padding: 20px 18px 16px;
  border: 1.5px solid rgba(255, 255, 255, 0.12);
  border-left: 4px solid rgb(var(--lib-hue, 99 53 240));
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.04);
  text-decoration: none; color: inherit;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.lp-libcard:hover {
  border-color: rgb(var(--lib-hue, 99 53 240) / 0.6);
  box-shadow: 0 8px 26px rgb(var(--lib-hue, 99 53 240) / 0.22);
  transform: translateY(-2px);
}
.lp-libcard__cat {
  align-self: flex-start;
  font-family: var(--font-mono); font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--eng-white);
  border: 1px solid rgb(var(--lib-hue, 99 53 240) / 0.6);
  background: rgb(var(--lib-hue, 99 53 240) / 0.22);
  border-radius: 999px; padding: 3px 10px;
}
.lp-libcard__title { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; line-height: 1.5; color: var(--eng-white); overflow-wrap: anywhere; }
/* UAT round 3.1: the card carries a taste of the content — the article's own
   quick answer (clamped, never rewritten), the page's real read-time in mono,
   and the skill's craft raster in a hue-glow ring (the same asset the fleet
   and the article hero fly — one identity, three surfaces). */
.lp-libcard__top { display: flex; align-items: center; gap: 10px; }
.lp-libcard__craft {
  width: 44px; height: 44px; margin-left: auto; flex: none;
  padding: 5px; border-radius: 50%;
  border: 1px solid rgb(var(--lib-hue, 99 53 240) / 0.35);
  background: radial-gradient(circle at 50% 50%, rgb(var(--lib-hue, 99 53 240) / 0.18), rgb(var(--lib-hue, 99 53 240) / 0) 70%);
}
.lp-libcard__ex {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden;
  font-size: 0.86rem; line-height: 1.65;
  color: rgba(255, 255, 255, 0.68);
}
.lp-libcard__meta {
  display: flex; gap: 12px; flex-wrap: wrap;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.72);
}
.lp-libcard__chip { white-space: nowrap; }
.lp-libcard__go { margin-top: auto; font-weight: 600; font-size: 0.9rem; color: var(--eng-green); }
.lp-library__more { text-align: center; margin-top: 22px; }
.lp-library__more a { font-weight: 700; color: var(--eng-white); text-decoration: underline; text-underline-offset: 4px; }

@media (prefers-reduced-motion: reduce) {
  /* PICKER stations: each rests at the frame where the thing has ARRIVED —
     the phrase is in the sentence, the gap is filled, the marker is on Part 2.
     Stopped mid-loop they would show a missing word and an empty slot, which
     is the problem, not the answer. */
  .lp-st1-slot, .lp-st1-word, .lp-st1-yours, .lp-st2-b, .lp-st2-bank, .lp-st2-gap, .lp-st2-fill,
  .lp-st3-here, .lp-st3-pin, .lp-st3-ring { animation: none; }
  .lp-st1-slot  { opacity: 0; }
  .lp-st1-word  { opacity: 1; transform: none; }
  .lp-st1-yours { opacity: .5; }
  .lp-st2-b     { opacity: .42; }
  .lp-st2-bank  { opacity: .42; }
  .lp-st2-gap   { opacity: 0; }
  .lp-st2-fill  { opacity: 1; transform: none; }
  .lp-st3-here  { opacity: 1; }
  .lp-st3-pin   { opacity: 1; transform: none; }
  .lp-st3-ring  { opacity: 0; }

  /* HOW panels: the resting state is the FINISHED diagram, not a blank one.
     Stopping these mid-story would leave step 1 with no answer picked and
     step 3 with tomorrow unchanged — which reads as three broken pictures
     rather than as motion being off. Each is forced to its arrived frame. */
  .lp-hd1-chosen, .lp-hd1-ring, .lp-hd1-dot, .lp-hd2-ring, .lp-hd2-dot,
  .lp-hd3-miss, .lp-hd3-carry, .lp-hd3-new, .lp-how__loop-chase { animation: none; }
  .lp-hd1-chosen { opacity: .34; }
  .lp-hd1-ring   { opacity: 1; }
  .lp-hd1-dot    { opacity: 1; transform: translateX(44px); }
  .lp-hd2-ring   { opacity: .95; }
  .lp-hd2-dot    { opacity: 1; transform: none; }
  .lp-hd3-miss   { opacity: 1; }
  .lp-hd3-carry  { opacity: 1; transform: translate(-84px, 4px); }
  .lp-hd3-new    { opacity: 1; }

  /* WHAT'S INSIDE: four ambient loops with no end state — stopping them is the
     whole fix. Two exceptions are forced to an ARRIVED state, because their
     resting frame is opacity 0 and a stopped icon must still READ: the "right
     option" stays lit on the first bar, and the coach's reply stays on screen.
     A reply that never arrives is not "motion disabled", it is a broken icon. */
  .lp-ico-card, .lp-ico-wave, .lp-ico-hit, .lp-ico-x, .lp-ico-why { animation: none; }
  .lp-ico-wave { transform: none; }
  .lp-ico-hit { opacity: 0; }
  /* NOT :first-of-type — the grey .lp-ico-opt bars are <rect> too, so
     "first of type" selected a grey one and the icon read as three blank
     options with no answer. An explicit class is the only unambiguous handle
     when siblings share a tag. */
  .lp-ico-hit--1 { opacity: 1; }
  /* The arrived state is the reason PRESENT. Frozen at scaleX(0) the card
     would illustrate the opposite of what it claims. */
  .lp-ico-x { opacity: 1; }
  .lp-ico-why { opacity: 1; transform: none; }

  .lp-sat { animation: none; }   /* B-5: Saturn float was ungated */
  .lp-hero__cue svg { animation: none; }          /* B-5: scroll-cue arrow bounce */
  /* the fleet must ARRIVE, not sit at the pad: `left` is overridden back to the
     destination and the transition removed, so the section reads as finished
     rather than as five marks that never moved */
  .lp-fleet.is-armed .lp-lane__craft,
  .lp-fleet.is-armed.is-launched .lp-lane__craft { left: 100%; transition: none; }
  .lp-fleet.is-armed .lp-lane__beam,
  .lp-fleet.is-armed.is-launched .lp-lane__beam { width: 100%; transition: none; }
  .lp-fleet.is-armed.is-launched .lp-fleet__sat { animation: none; }
  /* the ambient life around the destination stops; nothing here has an end
     state to force, so stopping IS the arrived state. The wave is an arrival
     event with no resting form, so it simply never plays. */
  .lp-map::before, .lp-fleet__halo, .lp-fleet__moonorbit,
  .lp-fleet.is-armed.is-launched .lp-lane__beam,
  .lp-fleet.is-armed.is-launched .lp-fleet__wave { animation: none; }
  /* the console un-arms itself: every panel back under its own heading, no
     selectors, no entrance — landing-console.js also returns early here, so
     this is the state a reduced-motion reader actually gets */
  .lp-console.is-armed .lp-console__routes { display: none; }
  .lp-console.is-armed .lp-console__panels { display: block; }
  .lp-console.is-armed .lp-console__panel { visibility: visible; opacity: 1; animation: none; }
  .lp-console.is-armed .lp-console__panel + .lp-console__panel { margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255, 255, 255, 0.07); }
  .lp-console.is-armed .lp-console__panelh { display: block; }
  .lp-console.is-armed .lp-console__cap { display: none; }
  .lp-console__lamp, .lp-console__scan { animation: none; }
  .lp-console__scan { display: none; }   /* a stopped sweep parked mid-glass is a smear, not a resting state */
  /* B2 additions. Each of these is ambient — it has no end state to reach, so
     stopping it is the whole fix; nothing needs repositioning the way the
     fleet's craft did. */
  .lp-hero::before { animation: none; }
  .lp-hero__blips i { animation: none; opacity: 0.55; }  /* still LIT, just not blinking */
  .lp-hero__h1 .eng-glow-text { animation: none; }
  .lp-flagship-demo { animation: none; border-color: color-mix(in srgb, var(--eng-green) 26%, transparent); }
  .lp-sat__ring, .lp-sat__front, .lp-sat__glint { animation: none; }
  /* B4. Ambient loops just stop. The two that have an END STATE are forced to
     it: the report bars must be at their real width (a bar frozen at scaleX(0)
     reports every skill as zero), and the ribbon must be visible. */
  .lp-how__spine::after, .lp-how__step .lp-how__n,
  .lp-how__lesson-dot, .lp-day__step::before,
  .lp-day__stdot, .lp-day__stnum, .lp-day__segfill, .lp-day__scene, .lp-day__ring .d-wave, .lp-day__step,
  .lp-picker__door::before, .lp-picker__go svg, .lp-cta__btn, .lp-cta__btn svg,
  .lp-cta::before, .lp-report__level-val { animation: none; }
  /* The day-ring's arrived state is the CLOSED loop, not a fifth of one: a
     frozen 100/403 arc is a fragment nobody drew on purpose. Complete the ring
     and leave the dot at station 1, which is where a day starts. The five
     stations stay in their unlit state — with nothing moving, no step is "now",
     and lighting one would claim a moment that isn't happening. */
  .lp-day__sweep { stroke-dasharray: none;
                   stroke: color-mix(in srgb, var(--glow-accent) 55%, transparent); }
  /* The static rotate STAYS. Dropping it left the pip parked between stations
     3 and 4 — a day frozen halfway through, which is not a resting state. With
     it, the pip sits on station 1, and the completed ring makes the rotation
     itself invisible. */
  .lp-report__fill, .lp-reveal.is-out .lp-report__fill { transform: none; transition: none; }
  .lp-report__ribbon, .lp-reveal.is-out .lp-report__ribbon { opacity: 1; transform: rotate(-8deg); transition: none; }
  /* the pass: the foil sweep and the craft on the leg are ambient, so they just
     stop; the checkpoint nodes have an END STATE and must be forced to it, or a
     reader sees five hollow checkpoints beside five full gauges. */
  .lp-report__sheen::before, .lp-reveal:not(.is-out) .lp-report__sheen::before { animation: none; }
  .lp-report__sheen { opacity: 0; }
  .lp-report__leg::after { animation: none; }
  .lp-report__node, .lp-reveal.is-out .lp-report__node {
    background: var(--eng-purple);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--eng-purple) 18%, transparent);
    transition: none;
  }
  /* Reveal: land immediately. `is-out` may already be on the element when the
     media query applies, so the ARRIVED state has to be forced, not merely
     un-transitioned — otherwise the block stays at opacity 0 forever. */
  /* Arrived state for the day-ring: all five segments lit (the loop is whole,
     not a fifth of one) and the first scene visible — an empty core would show
     a diagram claiming five steps while drawing none of them. */
  .lp-day__segfill { opacity: .85; }
  .lp-day__scene { opacity: 0; }
  .lp-day__scene--1 { opacity: 1; }
  .lp-day__ring .d-wave { transform: none; }
  .lp-reveal, .lp-reveal.is-out { opacity: 1; transform: none; transition: none; }
  /* library cards (UAT round 3): hover lift dies; static card survives */
  .lp-libcard { transition: none; }
  .lp-libcard:hover { transform: none; }
}
