/* ===========================================================================
   LearnLoop redesign layer, 2026-07-27.

   Loaded AFTER app.css, so it wins on equal specificity. Kept as a separate
   file on purpose: app.css still owns every existing component, this file only
   restyles and adds. Deleting this one line from index.html reverts the whole
   look without touching a single original rule.

   The token values themselves were retuned in app.css (light and vibrant now),
   which is what restyles the existing components; this file handles shape,
   type, and the new pieces: Loopy, Home, header nav, account menu, avatars.
   =========================================================================== */

/* ---- Type --------------------------------------------------------------- */
h1, h2, h3, h4,
.title, .brand-name, .primary-btn, .ghost-btn, .nav-item, .tab, .seg-btn {
  font-family: var(--display); letter-spacing: -.01em;
}
body { font-family: var(--sans); }

/* ---- Rounder, softer versions of the existing controls ------------------ */
.primary-btn, .ghost-btn, .chip-btn, .send-btn, .linkish {
  border-radius: 999px;
  transition: transform .12s ease, box-shadow .18s ease, background .18s ease, border-color .18s ease;
}
/* Tinted with the brand blue, and much softer than before: these were a violet
   glow left from the old theme, which read as a halo rather than elevation. */
.primary-btn { box-shadow: 0 2px 6px rgba(27, 111, 212, .18); }
.primary-btn:hover { box-shadow: 0 4px 12px rgba(27, 111, 212, .26); }
.primary-btn:active, .ghost-btn:active, .chip-btn:active { transform: translateY(1px) scale(.99); }
.icon-btn { border-radius: 12px; }
.card, .drawer, .sidebar, .console { border-radius: var(--radius); }

/* ---- Loopy, the studio's face ------------------------------------------- */
.loopy { display: block; }
.loopy-float { animation: ll-bob 4.5s ease-in-out infinite; }
@keyframes ll-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } }
.loopy .blink { animation: ll-blink 5.5s infinite; transform-origin: center; }
@keyframes ll-blink { 0%, 92%, 100% { transform: scaleY(1); } 96% { transform: scaleY(.1); } }
.loopy .beacon { animation: ll-beacon 2.4s ease-in-out infinite; }
@keyframes ll-beacon { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
@keyframes ll-pop { from { opacity: 0; transform: translateY(10px) scale(.97); } }

/* ---- "Can I speak?" -----------------------------------------------------
   Replaces the old "an agent wants to contribute" notice, and (2026-07-28) the
   "the arena is quiet" line as well. A named character asking a direct question
   is easier to act on than a notice about an abstraction, or about the room.
   The engine behaviour is unchanged: accepting is still what spends the LLM
   call, declining still dismisses. */
.ask-bubble {
  display: flex; align-items: center; gap: 14px; margin: 10px 0;
  background: color-mix(in srgb, var(--warn) 10%, var(--panel));
  border: 1.5px solid color-mix(in srgb, var(--warn) 34%, var(--line));
  border-radius: var(--radius); padding: 12px 16px;
  animation: ll-pop .32s cubic-bezier(.2, .9, .3, 1.2) both;
}
.ask-bubble .loopy { flex: none; }
/* The quiet nudge is the same component in a calmer key: nobody is waiting on
   an answer, so it should not look as urgent as an agent with something to say. */
.offer-bubble.ask-bubble.quiet {
  background: var(--panel-2);
  border-color: var(--line);
  border-left: 3px solid var(--ink-faint);
}
.ask-bubble .ask-copy { flex: 1; min-width: 0; }
.ask-bubble .ask-copy b { font-family: var(--display); font-size: 1rem; display: block; }
.ask-bubble .ask-copy p { margin: 2px 0 0; font-size: .85rem; color: var(--ink-soft); }
.ask-bubble .ask-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- Header nav --------------------------------------------------------- */
.top-nav { display: flex; gap: 4px; flex-wrap: wrap; }
.top-nav button {
  font-family: var(--display); font-size: .93rem; border: none; background: none;
  color: var(--ink-soft); padding: 8px 15px; border-radius: 999px; cursor: pointer;
  transition: background .16s ease, color .16s ease;
}
.top-nav button:hover { background: var(--panel-3); color: var(--ink); }
.top-nav button.active { background: var(--brand); color: var(--on-brand); }

/* ---- Theme toggle -------------------------------------------------------
   Sized and shaped to sit beside the avatar button without competing with it:
   same 38px round footprint, but a hairline border and no fill, so the picture
   stays the only thing with weight in that corner.

   Exactly one of the two icons is displayed at a time, chosen by the attribute
   the head script sets. Doing it in CSS rather than JS means the right icon is
   already correct in the first painted frame, same as the colours. */
.theme-btn {
  width: 38px; height: 38px; flex: none; display: grid; place-items: center;
  border: 1.5px solid var(--line); border-radius: 50%; cursor: pointer;
  background: var(--panel); color: var(--ink-soft); padding: 0;
  transition: border-color .16s ease, color .16s ease, background .16s ease;
}
.theme-btn:hover { border-color: var(--brand); color: var(--brand-ink); }
.theme-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.theme-ico { display: none; }
:root[data-theme="light"] .theme-ico.when-light,
:root[data-theme="dark"]  .theme-ico.when-dark { display: block; }

/* ---- Account menu ------------------------------------------------------- */
.me-menu { position: relative; }
/* A ring, not a gradient disc: the picture inside is the only colour here. */
.avatar-btn {
  border: 2px solid var(--line); padding: 2px; border-radius: 50%; cursor: pointer;
  display: inline-grid; background: var(--panel); line-height: 0;
  transition: border-color .16s ease;
}
.avatar-btn:hover, .avatar-btn[aria-expanded="true"] { border-color: var(--brand); }
.menu-pop button svg { flex: none; color: var(--ink-faint); }
.menu-pop button.danger svg { color: var(--bad); }
.menu-pop {
  position: absolute; right: 0; top: calc(100% + 10px); min-width: 224px; z-index: 70;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 8px; animation: ll-pop .18s ease both;
}
.menu-pop .menu-head {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px 12px;
  border-bottom: 1px solid var(--line); margin-bottom: 6px;
}
.menu-pop .menu-head b { display: block; font-family: var(--display); font-size: .95rem; }
.menu-pop button {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  border: none; background: none; cursor: pointer; font: inherit; font-size: .9rem;
  color: var(--ink); padding: 10px 12px; border-radius: 10px;
}
.menu-pop button:hover { background: var(--panel-3); }
.menu-pop button.danger { color: var(--bad-ink); }

/* ---- Export menu (author ask, 2026-08-04) -------------------------------- */
/* Borrows .menu-pop's look rather than its rules, because it anchors to a small
   ghost button in the topbar instead of the avatar, and it is narrower: two
   items, each a format rather than a destination. */
.export-menu { position: relative; display: inline-block; }
.export-pop {
  position: absolute; right: 0; top: calc(100% + 8px); min-width: 190px; z-index: 70;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 6px; animation: ll-pop .18s ease both;
}
.export-pop button {
  display: flex; align-items: baseline; gap: 7px; width: 100%; text-align: left;
  border: none; background: none; cursor: pointer; font: inherit; font-size: .88rem;
  color: var(--ink); padding: 9px 11px; border-radius: 9px;
}
.export-pop button:hover { background: var(--panel-3); }
.export-pop .muted { font-size: .78rem; }

/* ---- Avatars ------------------------------------------------------------ */
.avatar { border-radius: 50%; display: block; flex: none; background: var(--panel-3); object-fit: cover; }
/* An uploaded photo is layered OVER the drawn creature rather than replacing
   it, so a photo that has not downloaded (or 404s, or the learner is offline)
   shows the creature instead of a broken-image icon. */
.avatar-wrap { position: relative; display: inline-block; flex: none; line-height: 0; }
.avatar-wrap > * { position: absolute; inset: 0; width: 100%; height: 100%; }
/* auto-fill, not a fixed 6 columns. `repeat(6, 1fr)` reads as "six equal
   columns that shrink", but 1fr is minmax(auto, 1fr) and the automatic minimum
   is the swatch's own content size, so the columns stopped shrinking at 42px
   and the row simply ran off the edge: measured at a 320px viewport, six 42px
   columns plus gaps came to 292px inside a 225px card, putting the last swatch
   at x 293-335. auto-fill drops to as many columns as actually fit and wraps
   the rest onto a second row. */
.avatar-swatches {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
  gap: 8px; margin-top: 12px;
}
.avatar-swatches button {
  border: 2px solid transparent; background: none; border-radius: 50%; padding: 2px; cursor: pointer;
  transition: border-color .16s ease;
}
.avatar-swatches button:hover { border-color: var(--line); }
.avatar-swatches button.on { border-color: var(--brand); }

.user-row { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--line); }
.user-row:last-child { border-bottom: none; }
.user-row .nm { font-weight: 800; font-size: .92rem; }
.user-row .sub { font-size: .78rem; color: var(--ink-soft); }
.user-row .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--good); margin-left: auto; flex: none; }

/* ---- Home --------------------------------------------------------------- */
/* width:100% and min-width:0 are both load-bearing, and their absence was the
   home page "not rendering properly" after the 2026-07-30 deploy.

   This is a flex item (.main is a column flex container) with `margin: 0 auto`.
   Auto cross-axis margins suppress the stretch that would otherwise size it to
   the container, so its width falls back to fit-content, which is bounded by
   max-width but NOT by the viewport. That was harmless while the sections were
   wrapping grids with a small max-content. Turning them into rails introduced a
   1404px track, so fit-content jumped to the 1140px cap and stayed there at
   every viewport: measured at 768px wide, .home-wrap was 1140px and everything
   past 768 (including the robot) was silently clipped by .main's overflow-x.

   width:100% makes the width definite against the container instead, and
   min-width:0 stops the automatic minimum size from reintroducing the same
   content-driven floor. max-width still caps it on a large screen. */
.home-wrap {
  width: 100%; min-width: 0;
  max-width: 1140px; margin: 0 auto; padding: 34px 26px 60px;
}
.home-hero { display: grid; grid-template-columns: 1.1fr .9fr; gap: 30px; align-items: center; margin-bottom: 44px; }
.home-hero h1 { font-size: clamp(2rem, 4.6vw, 3.2rem); line-height: 1.1; margin: 14px 0 .35em; }
.home-hero .lede { font-size: 1.06rem; color: var(--ink-soft); max-width: 46ch; }

/* ---- Horizontal rails ---------------------------------------------------
   The rooms and the five-step pipeline are rails rather than wrapping grids.
   Both are short, ordered-ish sets where seeing that there IS more to the right
   is better than reflowing into a block of equal-weight tiles.

   .rail is the scroll container and the track inside it is a flex row of
   fixed-width cards. overflow-x lives HERE and nowhere above it, which is what
   keeps the page itself from ever scrolling sideways.

   The scrollbar is HIDDEN (author ask, 2026-07-30). What replaces it: the ends
   fade out, and a round arrow button sits over each end, each appearing only
   when there is actually something further that way. Keyboard and trackpad
   scrolling are untouched.

   The fade is a MASK, not a painted gradient. It varies opacity only and adds no
   colour, so it holds against any surface behind it and does not reintroduce the
   gradient fills that are banned everywhere else in the interface. The four
   states are separate rules rather than one clever formula because a
   mask-image has to be declared whole.

   scroll-snap makes a drag land on a card edge instead of halfway through one,
   and the -8px/8px padding pair stops a card's hover lift from being clipped by
   its own scroll container. */
.rail-sub { margin: -.5em 0 1.1em; }

.rail-wrap { position: relative; margin-bottom: 40px; }
.rail {
  overflow-x: auto; overflow-y: hidden;
  margin: 0 -8px; padding: 8px;
  scroll-snap-type: x proximity;
  /* Insets the snapport by the same 8px as the padding, so the first card snaps
     at scrollLeft 0. Without it the rail rested at scrollLeft 8 (measured
     2026-07-30), which read as "already scrolled": the prev arrow appeared and
     the left edge faded while sitting at the very start. */
  scroll-padding-inline: 8px;
  scroll-behavior: smooth;
  /* Chrome/Safari and Firefox respectively. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.rail::-webkit-scrollbar { display: none; }
.rail:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius); }

/* Fade whichever end has more content past it. The classes are set by
   redesign.js::updateRail on scroll and on resize. */
.rail-wrap.can-left .rail {
  mask-image: linear-gradient(to right, transparent 0, #000 68px, #000 100%);
}
.rail-wrap.can-right .rail {
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 68px), transparent 100%);
}
.rail-wrap.can-left.can-right .rail {
  mask-image: linear-gradient(to right, transparent 0, #000 68px,
                              #000 calc(100% - 68px), transparent 100%);
}

.rail-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 38px; height: 38px; border-radius: 50%; cursor: pointer;
  display: grid; place-items: center;
  background: var(--panel); color: var(--ink-soft);
  border: 1px solid var(--line); box-shadow: var(--shadow);
  opacity: 0; pointer-events: none;
  transition: opacity .18s ease, color .15s ease, border-color .15s ease;
}
.rail-arrow.prev { left: -6px; }
.rail-arrow.next { right: -6px; }
.rail-wrap.can-left .rail-arrow.prev,
.rail-wrap.can-right .rail-arrow.next { opacity: 1; pointer-events: auto; }
.rail-arrow:hover { color: var(--brand-ink); border-color: var(--brand); }
.rail-arrow svg { width: 18px; height: 18px; display: block; }

.home-rooms { display: flex; gap: 16px; }
.home-rooms .room-card { flex: 0 0 268px; scroll-snap-align: start; }
.home-flow { display: flex; gap: 14px; }
.home-flow .flow-step { flex: 0 0 216px; scroll-snap-align: start; }

/* A touch device has no hover and scrolls the rail directly, so the arrows are
   dead weight there. The fades stay: they still say there is more. */
@media (hover: none) {
  .rail-arrow { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .rail { scroll-behavior: auto; }
}

.room-card {
  text-align: left; cursor: pointer; border: 1px solid var(--line); background: var(--panel);
  border-radius: var(--radius); padding: 24px; box-shadow: var(--shadow); font: inherit; color: inherit;
  display: flex; flex-direction: column; gap: 6px;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.room-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); border-color: var(--brand); }
.room-card .ico {
  width: 46px; height: 46px; border-radius: 14px; display: grid; place-items: center;
  background: var(--panel-3); color: var(--ink-soft);
  transition: background .18s ease, color .18s ease;
}
.room-card .ico svg { width: 22px; height: 22px; display: block; }
.room-card:hover .ico { background: color-mix(in srgb, var(--brand) 12%, transparent); color: var(--brand-ink); }
.room-card h3 { margin: 6px 0 0; }
.room-card p { margin: 0; font-size: .9rem; color: var(--ink-soft); }
.room-card .go { margin-top: auto; padding-top: 12px; color: var(--brand); font-weight: 700; font-size: .87rem; }

/* Numbered because the turn pipeline genuinely IS ordered. Nothing else in the
   interface is numbered, because nothing else is a sequence. */
.flow-step { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; }
/* --brand-ink, not --brand: the badge tints its own background with the same
   hue, so the plain brand on top of it measured 4.18:1 (light) and 4.3:1 (dark),
   just under AA. The darker step clears it on both without changing the look. */
.flow-step .n {
  font-family: var(--mono); font-size: .74rem; font-weight: 700; color: var(--brand-ink);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border-radius: 8px; padding: 3px 8px; display: inline-block; margin-bottom: 10px;
}
.flow-step h4 { margin: 0 0 4px; font-size: .98rem; }
.flow-step p { margin: 0; font-size: .85rem; color: var(--ink-soft); }

.home-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.home-stat { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px 20px; }
.home-stat b { font-family: var(--display); font-size: 1.7rem; display: block; line-height: 1.1; }
.home-stat span { font-size: .82rem; color: var(--ink-soft); }

/* ---- Hint beside Send ---------------------------------------------------
   Deliberately not styled as a primary action: asking for a hint is a choice
   the learner makes, not the default path through the interface. */
.hint-stack { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.hint-rungs { display: flex; gap: 3px; }
.hint-rungs i { width: 12px; height: 4px; border-radius: 2px; background: var(--line); transition: background .2s ease; }
.hint-rungs i.on { background: var(--warn); }
/* #4A3000 measured 3.9:1 on --warn; this clears 4.6:1 on both themes' amber. */
.btn-hint { background: var(--warn); color: #3A2400; border: none; }
.btn-hint:hover { filter: brightness(1.04); }

/* ---- Login --------------------------------------------------------------
   Two columns as in the prototype: Loopy on the left, the form on the right.

   Both columns stretch to the full height and centre their own contents, rather
   than the screen centring two items of whatever height they happen to be. The
   art column is also the only thing allowed to be cropped: `overflow: hidden`
   plus a viewport-relative robot means a short window loses a little of the
   illustration instead of pushing it out of the page.

   The split is an even 1fr 1fr (it was 1.05fr .95fr): each half now carries one
   thing sized to fill it, the robot on the left and the sign-in card on the
   right, so an uneven split just shows as a lopsided gutter. */
.auth-split {
  display: grid; grid-template-columns: 1fr 1fr;
  place-items: stretch; align-content: stretch;
  min-height: 100%; width: 100%;
}
.auth-art {
  position: relative; display: grid; place-items: center;
  padding: 32px; overflow: hidden; text-align: center; min-height: 0;
}
/* The side padding is the only gutter between the card and the window edge, so
   it is clamped rather than fixed: it opens up on a wide monitor and closes to
   24px on a phone, where the card is the whole page. */
.auth-form-col {
  display: grid; place-items: center;
  padding: 40px clamp(24px, 3.4vw, 64px); min-height: 0;
}

/* Wider than the 430px in app.css, so the card fills its half of the window
   instead of floating as a narrow strip in the middle of it. The cap is a
   readability limit, not a design one: past ~600px the email and password
   fields start reading as a form the eye has to track across. */
.auth-card { width: min(600px, 100%); padding: 34px 38px; }
.auth-card .auth-heading { font-size: 25px; }
.auth-card .auth-sub { font-size: 13.5px; }

/* Blurred discs, the only ambient decoration on the page. They are
   position:absolute and MUST stay that way: a rule of `.auth-art > *` once made
   them relative, which turned three decorations into three grid rows and shoved
   the robot 543px down, off the bottom of the window (fixed 2026-07-28). Hence
   the :not() below rather than a blanket child selector. */
.auth-orb { position: absolute; border-radius: 50%; filter: blur(52px); opacity: .38; }
.auth-orb.o1 { width: 340px; height: 340px; background: #C9BCFF; top: 6%; left: 2%; }
.auth-orb.o2 { width: 300px; height: 300px; background: #FFD79B; bottom: 8%; right: 6%; }
.auth-orb.o3 { width: 230px; height: 230px; background: #A8E6FF; top: 46%; right: 30%; }
.auth-art > :not(.auth-orb) { position: relative; z-index: 1; }

.auth-art-inner {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  width: 100%; max-width: 620px;
}
/* Sized against the SMALLER of the two viewport axes, so the robot fills its
   half of a wide window but still shrinks on a short screen instead of running
   off the bottom of it. min() rather than a single unit because either axis can
   be the binding one: 34vw is what governs a laptop, 48vh a half-height window.
   Previously 26vh, which left it at ~230px in a 756px column. */
.auth-art .loopy { width: clamp(180px, min(34vw, 48vh), 460px); height: auto; }
.auth-art .art-copy { max-width: 460px; margin-top: 10px; }
.auth-art .art-copy h2 { font-size: clamp(1.3rem, 2.7vh, 1.9rem); }
.auth-art .art-copy p { font-size: .95rem; }

/* ---- Shared: toggle chips ------------------------------------------------
   Used by the questionnaire and the profile's languages field, so a language
   picked in one looks identical in the other. A border and a tinted fill carry
   the selected state; there is no gradient and no colour-only signal, since
   aria-pressed is on the element too. */
.chip-set, .onb-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip-toggle {
  font: inherit; font-size: .9rem; font-family: var(--display);
  padding: 9px 16px; border-radius: 999px; cursor: pointer;
  background: var(--panel); color: var(--ink-soft);
  border: 1.5px solid var(--line);
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.chip-toggle:hover { border-color: var(--hover-line); color: var(--ink); }
.chip-toggle.on {
  background: var(--brand-soft); border-color: var(--brand); color: var(--brand-ink);
  font-weight: 700;
}

/* A group from an earlier session, listed alongside the live ones. Opening it
   rehydrates it from its trace, so the tag says "earlier", not "read only". */
.g-past {
  margin-left: 8px; font-size: .68rem; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-faint);
  background: var(--panel-3); padding: 2px 7px; border-radius: 99px;
}

/* ---- Profile: unfinished-setup banner ----------------------------------- */
.onb-banner {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--panel-2); border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: var(--radius-sm); padding: 13px 15px; margin-bottom: 16px;
}
.onb-banner > div { flex: 1; min-width: 200px; }
.onb-banner b { font-family: var(--display); font-size: .95rem; }
.onb-banner p { margin: 2px 0 0; }

/* ---- First-run questionnaire --------------------------------------------
   Full-screen, one question at a time. position:fixed with its own scroll, for
   the same reason the login screen has one: a tall step on a short window must
   have somewhere to go rather than being clipped.

   The frame is a column of four fixed pieces (head, rail, stage, foot) with only
   the stage flexing, so Continue never moves between steps. A button that shifts
   position as the content changes height is the thing that makes a multi-step
   flow feel unstable. */
.onb-screen {
  position: fixed; inset: 0; z-index: 110;
  background: var(--bg); overflow-y: auto;
  display: grid; place-items: stretch;
}
.onb-frame {
  width: min(680px, 100%); margin: 0 auto; min-height: 100%;
  display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto;
  padding: 18px 26px 20px; gap: 16px;
}
.onb-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.onb-skip { font-size: .84rem; }

/* Progress: one segment per step the learner will actually see. Height and
   colour only, no numbers, because the count is spelled out in the footer. */
.onb-rail { display: flex; gap: 6px; }
.onb-rail i {
  flex: 1; height: 4px; border-radius: 999px; background: var(--line);
  transition: background .25s ease;
}
.onb-rail i.done { background: color-mix(in srgb, var(--brand) 45%, var(--line)); }
.onb-rail i.now { background: var(--brand); }

/* THE CONTINUE-BUTTON OVERLAP (author report, 2026-07-30 third session,
   reproduced and measured before fixing).

   The stage row is minmax(0, 1fr). A zero minimum means the TRACK is allowed to
   be shorter than the card in it, so a tall step did not push the frame taller,
   it simply overflowed its own track and painted straight over the footer. And
   because the frame's height was still exactly the viewport, the scroll
   container never grew either, so there was no scrolling out of it. Measured at
   1280x620: the tone step's card ended 64px past the top of the footer and the
   hint-ladder step 104px past it, with #onb-screen.scrollHeight still 620.
   That is the reported "Continue goes underneath the options".

   The fix keeps the design promise (the footer is a fixed piece, so Continue
   never moves between steps) and makes the STAGE the thing that scrolls:

   - overflow-y: auto gives the overflow somewhere to go inside the track. A
     scroll container's automatic minimum size is zero, so this also stops the
     card from ever contributing height to the frame again.
   - `safe center` keeps a short step optically centred but falls back to `start`
     the moment the content is taller than the track. Plain `center` centres the
     overflow too, which loses the TOP of a tall step off the upper edge, where
     there is no scrollbar position to recover it from.
   - scroll-padding keeps the first and last line off the clipped edges. */
.onb-stage {
  display: grid; align-content: safe center; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  scroll-padding-block: 6px;
}
.onb-card { padding: 4px 0; }
/* The step transition: content rises and fades in. 18px is enough to read as
   movement without the eye having to chase it. */
.onb-card.in { animation: onb-in .34s cubic-bezier(.22, .85, .3, 1) both; }
@keyframes onb-in { from { opacity: 0; transform: translateY(18px); } }

/* Vertical rhythm here is deliberately tight (author report, 2026-07-30 third
   session: "the options are taking up too much space"). Every value below was
   reduced against a measured target: the tallest step, the five-rung hint
   ladder, has to fit a 620px-high window without the stage needing to scroll,
   because a laptop with a bookmarks bar is the common case rather than the edge
   one. The type sizes are still the display scale, just one notch down. */
.onb-eyebrow {
  display: block; font-size: .72rem; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--brand); margin-bottom: 7px;
}
.onb-title { font-family: var(--display); font-size: clamp(1.4rem, 3vw, 1.8rem);
             line-height: 1.15; margin: 0 0 .3em; }
.onb-why { color: var(--ink-soft); font-size: .93rem; margin: 0 0 16px; max-width: 52ch; }
.onb-body { display: grid; gap: 12px; }
.onb-aside { font-size: .82rem; margin: 4px 0 0; max-width: 52ch; }
.onb-label { font-size: .82rem; font-weight: 700; color: var(--ink-soft); }

.onb-input {
  width: 100%; font: inherit; font-size: 1rem; color: var(--ink);
  background: var(--panel); border: 1.5px solid var(--line);
  border-radius: var(--radius-sm); padding: 13px 15px; resize: vertical;
  transition: border-color .15s ease;
}
.onb-input:focus { outline: none; border-color: var(--brand); }

/* A stack of full-width answers rather than a radio list: the whole row is the
   target, and each one has room for a line of explanation under its label. */
.onb-choices { display: grid; gap: 8px; }
.onb-choice {
  display: grid; gap: 1px; text-align: left; font: inherit; cursor: pointer;
  background: var(--panel); border: 1.5px solid var(--line);
  border-radius: var(--radius-sm); padding: 10px 14px; color: var(--ink);
  transition: border-color .15s ease, background .15s ease;
}
.onb-choice:hover { border-color: var(--hover-line); }
.onb-choice b { font-family: var(--display); font-size: 1rem; }
.onb-choice span { font-size: .86rem; color: var(--ink-soft); }
.onb-choice.on { border-color: var(--brand); background: var(--brand-soft); }
.onb-choice.on b { color: var(--brand-ink); }

.onb-sliders { display: grid; gap: 7px; }
.onb-slider {
  display: grid; grid-template-columns: minmax(84px, 1fr) minmax(96px, 1.5fr) 104px;
  gap: 12px; align-items: center; font-size: .88rem;
}
.onb-slider-name { min-width: 0; }
.onb-slider input[type=range] { width: 100%; accent-color: var(--brand); margin: 0; }
.onb-slider b {
  font-family: var(--display); color: var(--brand-ink); font-size: .82rem;
  text-align: right; min-width: 0;
}
/* Too narrow for three columns: the slider takes its own line under the name
   and the word, which is the stacked layout again but only where it is needed. */
@media (max-width: 520px) {
  .onb-slider { grid-template-columns: 1fr auto; gap: 4px 10px; }
  .onb-slider input[type=range] { grid-column: 1 / -1; }
}

.onb-ladder { display: grid; gap: 6px; }
.onb-rung {
  display: flex; align-items: center; gap: 11px; text-align: left; font: inherit;
  cursor: pointer; background: var(--panel); border: 1.5px solid var(--line);
  border-radius: var(--radius-sm); padding: 8px 13px; color: var(--ink);
  font-size: .9rem; transition: border-color .15s ease, background .15s ease;
}
.onb-rung:hover { border-color: var(--hover-line); }
.onb-rung.on { border-color: var(--brand); background: var(--brand-soft); color: var(--brand-ink); }
.onb-rung-n {
  flex: none; width: 26px; height: 26px; border-radius: 50%; display: grid;
  place-items: center; background: var(--panel-3); color: var(--ink-soft);
  font-family: var(--mono); font-size: .78rem; font-weight: 700;
}
.onb-rung.on .onb-rung-n { background: var(--brand); color: var(--on-brand); }

.onb-foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.onb-count { font-family: var(--mono); font-size: .78rem; }
.onb-foot .primary-btn { padding: 11px 24px; }
.onb-foot .primary-btn:disabled { opacity: .45; cursor: default; box-shadow: none; }

@media (max-width: 560px) {
  .onb-frame { padding: 16px 16px 20px; gap: 16px; }
  .onb-why { margin-bottom: 18px; }
}
/* A short window: the framing gives up space before the answers do. The stage
   scrolls if it still does not fit (see .onb-stage), so this is about how often
   that is needed, not about whether the step stays usable. */
@media (max-height: 700px) {
  .onb-frame { padding-top: 14px; padding-bottom: 16px; gap: 13px; }
  .onb-title { font-size: 1.35rem; margin-bottom: .22em; }
  .onb-why { font-size: .88rem; margin-bottom: 12px; }
  .onb-eyebrow { margin-bottom: 5px; }
  .onb-aside { font-size: .78rem; }
}
/* Short AND wide: the answer stacks pay for the height in a column of width they
   are not using. Two columns halves the tallest steps (tone 4 rows to 2, the
   hint ladder 5 to 3) and is what keeps them off the stage scrollbar entirely at
   a 500px-high window. Only applied where there is real width to give: below
   640px a two-column choice is narrower than its own label.

   The ladder keeps its order under wrapping because each rung carries its number
   as a visible badge, so 1 2 / 3 4 / 5 still reads as a ladder rather than a
   set. */
@media (max-height: 640px) and (min-width: 640px) {
  .onb-choices, .onb-ladder { grid-template-columns: 1fr 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .onb-card.in { animation: none !important; }
  .onb-rail i { transition: none !important; }
}

/* A snapped half-screen window. The hero stays TWO columns here and just gives
   the robot less room (author report, 2026-07-30: stacking it turned the hero
   into "the logo displayed below the main text like they are being
   containerized one after another", a 320px block of decoration pushing the
   real content up). */
@media (max-width: 900px) {
  .home-hero { grid-template-columns: 1.45fr 1fr; gap: 20px; margin-bottom: 34px; }
  .home-hero svg.loopy { height: 210px; }
  .home-hero h1 { font-size: clamp(1.6rem, 4.4vw, 2.3rem); }
  /* The art half goes away rather than stacking: on a phone it would push the
     form below the fold, which is the one thing the login screen must not do. */
  .auth-split { grid-template-columns: 1fr; }
  .auth-split .auth-art { display: none; }
  /* `.top-nav { display: none }` used to live here. It was half of the
     side-snap bug: at a snapped width the header links vanished at the same
     moment the sidebar became an off-canvas drawer, leaving no visible
     navigation at all. The nav is two buttons now and stays at every width. */
}
/* Genuinely narrow: one column, and the robot goes rather than stacking. It is
   decoration, and the sentence under it is the actual content of the page. */
@media (max-width: 620px) {
  .home-hero { grid-template-columns: 1fr; }
  .home-hero svg.loopy { display: none; }
  .home-wrap { padding: 24px 16px 44px; }
}
@media (max-width: 560px) {
  .top-nav button { padding: 7px 12px; font-size: .88rem; }
}
/* A short window (a small laptop, a docked terminal, a snapped half-screen).
   THE ROBOT SHRINKS, THE WORDS STAY.

   This rule used to `display: none` the copy at 620px and leave the robot at
   full size, which had the priority exactly backwards (author report,
   2026-08-03: on a small laptop "the text under the loopy icon is
   disappearing"). Loopy is decoration; "Meet Loopy / six tutors on standby, they
   ask before they speak" is the only thing on that half of the page that says
   what LearnLoop actually is, and it is the half of the pair a first-time
   visitor needs. A 1366x768 laptop has roughly 600px of viewport left once the
   browser chrome is taken off, so this fired on an ordinary machine rather than
   on an edge case, and the illustration it protected had ~300px of slack to
   give up. Shrinking it frees more room than hiding two lines of text ever did.

   Raised to 760px for the same reason: at 700px the pair was already tight
   enough that the copy sat hard against the bottom of the column. */
@media (max-height: 760px) {
  .auth-art { padding: 18px 24px; }
  .auth-art .loopy { width: clamp(120px, min(24vw, 32vh), 300px); }
  .auth-art .art-copy { margin-top: 6px; }
  .auth-art .art-copy h2 { font-size: 1.15rem; }
  .auth-art .art-copy p { font-size: .86rem; line-height: 1.45; }
  .auth-form-col { padding: 20px clamp(20px, 3vw, 48px); }
}
/* Genuinely no room left (a phone in landscape, a quarter-height window). NOW
   the copy goes, because below this the two would overlap rather than fit. */
@media (max-height: 430px) {
  .auth-art .art-copy { display: none; }
  .auth-art .loopy { width: clamp(80px, 34vh, 170px); }
}

@media (prefers-reduced-motion: reduce) {
  .loopy-float, .loopy .blink, .loopy .beacon, .ask-bubble { animation: none !important; }
  /* The sidebar still minimises, it just arrives there in one step. */
  .app, .sidebar { transition: none !important; }
}

/* ---- Career guidance ----------------------------------------------------
   Two columns: the mentor's answer and the fit dashboard on the left, the rail
   of past analyses on the right. The rail is narrower on purpose, it is an
   index rather than content. */
.career-stage { grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr); align-items: start; }
.career-side .h-item { width: 100%; }
.career-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.career-head h3 { margin: 0; }

.fit-dash { display: flex; gap: 22px; align-items: center; flex-wrap: wrap; margin: 14px 0 10px; }
.fit-chart { flex: none; line-height: 0; }
.fit-big { font-family: var(--display); font-size: 22px; font-weight: 700; fill: var(--ink); }
.fit-small { font-family: var(--sans); font-size: 10.5px; fill: var(--ink-faint); }
.fit-legend { list-style: none; margin: 0; padding: 0; flex: 1; min-width: 210px; display: grid; gap: 8px; }
.fit-legend li { display: flex; align-items: center; gap: 10px; font-size: .88rem; }
.fit-dot { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.fit-name { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.fit-name .muted { font-size: .76rem; }
.fit-pct { font-family: var(--mono); font-size: .82rem; font-weight: 700; flex: none; }
.career-past { text-align: left; }

/* Markdown blocks the mentor produces. The wrapper scrolls rather than the
   page: a wide table must never push the whole layout sideways. */
.md-table-wrap { overflow-x: auto; margin: 10px 0; }
.md-table { border-collapse: collapse; width: 100%; font-size: .86rem; }
.md-table th, .md-table td {
  border: 1px solid var(--line); padding: 7px 10px; text-align: left; vertical-align: top;
}
.md-table th { background: var(--panel-2); font-weight: 700; }
.md-list { margin: 8px 0; padding-left: 22px; display: grid; gap: 4px; }
.md-list li { line-height: 1.5; }

@media (max-width: 1100px) {
  .career-stage { grid-template-columns: 1fr; }
}

/* ---- Notifications ------------------------------------------------------
   Sits between the theme toggle and the picture (author ask, 2026-08-02).
   Matched to .theme-btn's geometry exactly (38px circle, same border weight)
   so the three header controls read as one row rather than three sizes. */
.notif-menu { position: relative; }
.notif-btn {
  position: relative; width: 38px; height: 38px; flex: none; display: grid;
  place-items: center; border: 1.5px solid var(--line); border-radius: 50%;
  cursor: pointer; background: var(--panel); color: var(--ink-soft); padding: 0;
  transition: border-color .16s ease, color .16s ease, background .16s ease;
}
.notif-btn:hover, .notif-btn[aria-expanded="true"] { border-color: var(--brand); color: var(--brand-ink); }
.notif-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The count lives INSIDE the button rather than floating beside it, so it can
   never drift away from the icon when the header reflows at a narrow width. */
.notif-badge {
  position: absolute; top: -3px; right: -3px; min-width: 17px; height: 17px;
  padding: 0 4px; border-radius: 99px; background: var(--bad); color: #fff;
  font-size: 10.5px; font-weight: 700; line-height: 17px; text-align: center;
  border: 2px solid var(--panel); box-sizing: content-box;
}
.notif-pop { min-width: 310px; max-width: min(360px, calc(100vw - 32px)); }
.notif-pop .menu-head { justify-content: space-between; }
/* .menu-pop button is a full-width menu ROW; the buttons in here are controls
   sitting beside text, so the row rules are undone rather than inherited. */
.notif-pop .menu-head button, .notif-pop .notif-row button {
  width: auto; flex: none; padding: 5px 12px;
}
#notif-list { max-height: min(60vh, 420px); overflow-y: auto; overscroll-behavior: contain; }
.notif-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px;
  border-radius: 10px;
}
.notif-row + .notif-row { border-top: 1px solid var(--line); border-radius: 0; }
.notif-row.unread { background: color-mix(in srgb, var(--brand) 8%, transparent); }
.notif-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.notif-text b { font-size: .88rem; line-height: 1.35; }
.notif-row .small-note { font-size: 11.5px; }
