/* ==========================================================================
   AscendSAT — app shell: sidebar, topbar, page canvas
   ========================================================================== */

#app {
  display: flex;
  min-height: 100vh;
}

/* ==========================================================================
   Sidebar
   ========================================================================== */

.sidebar {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: var(--sidebar-w);
  background: #fff;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  padding: 18px 12px 14px;
  z-index: 60;
  overflow-x: hidden;          /* labels shrinking out must never spill */
  transition: width var(--collapse-t);
}

/* Held only while the rail is actually moving. Animating width and margin
   relayouts the page every frame; the hint lets the compositor prepare for it
   once instead of rediscovering it each frame, and it is dropped afterwards so
   the layers do not stay promoted for the rest of the session. */
#app.collapsing .sidebar { will-change: width; }
#app.collapsing .main { will-change: transform; }

/* --- Brand --------------------------------------------------------------- */
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 8px 12px;
  cursor: pointer;
  user-select: none;
  transition: gap var(--collapse-t), padding var(--collapse-t);
}
.brand-mark {
  width: 38px; height: 38px;
  flex: none;
  display: grid; place-items: center;
  /* The character is cut out on transparency, so it needs no tile behind it
     and no rounded corners to hide a crop — it just stands next to the
     wordmark. `contain` keeps him whole; `cover` would clip his hat. */
}
.brand-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }
.brand-mark img { width: 100%; height: 100%; object-fit: cover; display: block; }
.brand-mark svg { width: 38px; height: 38px; filter: drop-shadow(0 1px 2px rgba(15, 23, 42, .18)); }
.brand-name {
  font-family: var(--font-brand);
  font-size: 23px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: var(--blue);
  white-space: nowrap;
}
.brand-name b { color: var(--indigo); font-weight: 800; }

/* --- Nav ----------------------------------------------------------------- */
/* min-height: 0 is load-bearing — without it a flex item refuses to shrink
   below its content height, so on a short window the nav pushed the footer
   down instead of scrolling and Compete ended up clipped off the bottom. */
.nav {
  display: flex; flex-direction: column; gap: 2px;
  overflow-y: auto;
  min-height: 0;
}
.nav::-webkit-scrollbar { width: 0; }

.nav-item {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 8.5px 14px;
  border-radius: var(--r-sm);
  border: 1.5px solid transparent;
  color: var(--slate-700);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t), color var(--t), border-color var(--t),
              padding var(--collapse-t), gap var(--collapse-t);
  white-space: nowrap;
  user-select: none;
}
.nav-item svg { width: 21px; height: 21px; flex: none; color: var(--slate-500); transition: color var(--t); }
.nav-item:hover { background: var(--slate-100); color: var(--ink); }
.nav-item:hover svg { color: var(--slate-700); }
.nav-item.on {
  background: var(--blue-tint);
  border-color: rgba(0, 166, 255, .35);
  color: var(--blue-dark);
  font-weight: 700;
}
.nav-item.on svg { color: var(--blue); }
.nav-item .dot {
  margin-left: auto;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--red);
  flex: none;
}

/* --- Exam switch (SAT | AP …) ---------------------------------------------
   Only rendered when an add-on registers a second mode (see js/modes.js).

   A recessed track with a raised, coloured thumb that springs between the
   segments. The thumb is positioned from two custom properties — how many
   segments, and which one is active — so it works for any number of modes,
   and it moves by transform alone so the slide stays on the compositor.
   Each mode can recolour the thumb with --mode-a / --mode-b; SAT's are here,
   an add-on sets its own. */
.mode-sw {
  --mode-a: #19B4FF; --mode-b: #2C76FF;
  position: relative; display: grid; grid-template-columns: repeat(var(--n, 2), 1fr);
  padding: 4px; margin: 2px 2px 14px;
  border-radius: 14px;
  background: linear-gradient(180deg, #EBEFF5, #F3F5F9);
  box-shadow: inset 0 1px 2px rgba(15, 23, 42, .09), inset 0 0 0 1px rgba(15, 23, 42, .05);
  flex: none;
}
.mode-thumb {
  position: absolute; top: 4px; bottom: 4px; left: 4px;
  width: calc((100% - 8px) / var(--n, 2));
  border-radius: 10px;
  background: linear-gradient(135deg, var(--mode-a), var(--mode-b));
  box-shadow: 0 4px 12px -2px color-mix(in srgb, var(--mode-b) 55%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .28);
  transform: translateX(calc(100% * var(--at, 0)));
  transition: transform .42s cubic-bezier(.34, 1.36, .5, 1), background .3s ease, box-shadow .3s ease;
}
.mode-opt {
  position: relative; z-index: 1;
  border: 0; background: none; cursor: pointer;
  padding: 9px 0;
  font: 800 13px var(--font-head, var(--font-body)); letter-spacing: .1em;
  color: var(--slate-500);
  transition: color .25s ease;
}
.mode-opt:hover:not(.on) { color: var(--slate-800, #1E293B); }
.mode-opt.on { color: #fff; text-shadow: 0 1px 1px rgba(0, 0, 0, .12); }
.mode-opt:focus-visible { outline: 2px solid var(--mode-b); outline-offset: 2px; border-radius: 10px; }
/* Collapsed there is room for one segment: the current mode, which flips to
   the next when pressed. */
.sidebar.mini .mode-sw { grid-template-columns: 1fr; width: 44px; margin-inline: auto; padding: 3px; border-radius: 12px; }
.sidebar.mini .mode-thumb { width: auto; right: 3px; left: 3px; top: 3px; bottom: 3px; transform: none; border-radius: 9px; }
.sidebar.mini .mode-opt:not(.on) { display: none; }
.sidebar.mini .mode-opt.on { padding: 8px 0; font-size: 11px; letter-spacing: .03em; }

.nav-divider { height: 1px; background: var(--line); margin: 6px 4px; }

/* --- Sidebar footer ------------------------------------------------------ */
.side-foot { flex: none; margin-top: auto; padding-top: 10px; display: grid; gap: 8px; }

.collapse-btn {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 8px 12px;
  border: 0;
  background: none;
  border-radius: var(--r-sm);
  color: var(--slate-400);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t), color var(--t),
              gap var(--collapse-t), padding var(--collapse-t);
}
.collapse-btn:hover { background: var(--slate-100); color: var(--slate-600); }
.collapse-btn svg { width: 18px; height: 18px; flex: none; }

.me-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--t), border-color var(--t),
              gap var(--collapse-t), padding var(--collapse-t);
  border: 1px solid transparent;
}
.me-chip:hover { background: var(--slate-50); border-color: var(--line); }
.me-chip .txt { min-width: 0; flex: 1; }
.me-chip .nm { font-size: 13.5px; font-weight: 700; line-height: 1.2; }
.me-chip .lv { font-size: 11.5px; color: var(--ink-3); font-weight: 600; }
.me-chip .cv { width: 16px; height: 16px; color: var(--slate-400); flex: none; }

/* --- Collapsed state -------------------------------------------------------
   Labels shrink+fade instead of snapping via display:none, so the collapse
   reads as one smooth motion rather than a width tween plus a hard cut. --- */
.brand-name,
.nav-item span,
.collapse-btn span,
.me-chip .txt,
.me-chip .cv,
.upgrade span {
  opacity: 1;
  max-width: 160px;
  overflow: hidden;
  transition: opacity var(--collapse-t), max-width var(--collapse-t),
              margin var(--collapse-t);
}
.sidebar.mini { width: var(--sidebar-w-collapsed); }
.sidebar.mini .brand { justify-content: center; padding-left: 0; padding-right: 0; }
.sidebar.mini .brand-name,
.sidebar.mini .nav-item span,
.sidebar.mini .collapse-btn span,
.sidebar.mini .me-chip .txt,
.sidebar.mini .me-chip .cv,
.sidebar.mini .upgrade span {
  opacity: 0; max-width: 0; margin: 0;
  /* Labels fade over the first two-thirds of the collapse, so the text is gone
     before the rail reaches its final width instead of being visibly clipped
     against the edge on the last few frames. */
  transition: opacity calc(var(--collapse-d) * .55) var(--collapse-e),
              max-width var(--collapse-t), margin var(--collapse-t);
}
.sidebar.mini .brand,
.sidebar.mini .nav-item,
.sidebar.mini .collapse-btn,
.sidebar.mini .me-chip,
.sidebar.mini .promo,
.sidebar.mini .upgrade { justify-content: center; padding-left: 0; padding-right: 0; }
/* The collapsed label still occupies its flex gap, which nudged every icon a
   few pixels left of the rail's true centre. Closing the gap is what actually
   lines the brand, the nav icons, the avatar and the CTA up with each other. */
.sidebar.mini .brand,
.sidebar.mini .nav-item,
.sidebar.mini .collapse-btn,
.sidebar.mini .me-chip,
.sidebar.mini .promo,
.sidebar.mini .upgrade { gap: 0; }
/* Collapsed, the upgrade CTA is just its crown — shrink it to a square badge
   so it reads as one more rail item instead of a full-width slab. */
.sidebar.mini .upgrade {
  width: 40px; height: 40px; padding: 0;
  margin-inline: auto;
  border-radius: 12px;
  box-shadow: 0 3px 10px rgba(0, 166, 255, .3);
}
.sidebar.mini .upgrade.pro { box-shadow: 0 3px 10px rgba(124, 92, 246, .3); }
.sidebar.mini .upgrade svg { width: 19px; height: 19px; }
.sidebar.mini .upgrade:hover { transform: translateY(-1px); }
.sidebar.mini .collapse-btn svg { transform: rotate(180deg); }
.collapse-btn svg { transition: transform var(--collapse-t); }

/* --- Upgrade CTA --------------------------------------------------------- */
.upgrade {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-sizing: border-box;
  width: 100%;
  /* Pinned height, like the promos: the collapse animates between two known
     sizes rather than snapping out of `auto`. */
  height: 46px;
  padding: 0 14px;
  border: 0;
  border-radius: var(--r-md);
  background: var(--blue);
  color: #fff;
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 800;
  letter-spacing: .05em;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t), width var(--collapse-t), height var(--collapse-t),
              padding var(--collapse-t), border-radius var(--collapse-t),
              margin var(--collapse-t),
              transform .16s var(--ease-out), box-shadow .16s var(--ease-out);
  box-shadow: var(--sh-blue);
}
.upgrade:hover { background: var(--blue-hover); transform: translateY(-2px); box-shadow: 0 10px 24px rgba(0, 166, 255, .38); }
.upgrade:active { transform: translateY(0) scale(.97); box-shadow: var(--sh-xs); transition-duration: .08s; }
.upgrade svg { width: 17px; height: 17px; }
.upgrade.pro {
  background: linear-gradient(90deg, var(--indigo), var(--purple));
  box-shadow: 0 4px 14px rgba(124, 92, 246, .3);
}

/* --- Sidebar promos --------------------------------------------------------
   Small on purpose. These sit directly under the nav, and the nav must always
   win the fight for vertical space — a promo that pushes Compete out of view
   has cost more than it earns. Height is pinned rather than left to content
   so collapsing to the mini rail is a real tween between two known sizes
   instead of a jump from `auto`. --------------------------------------------- */
.promo {
  position: relative;
  display: flex; align-items: flex-start; gap: 11px;
  box-sizing: border-box;
  width: 100%;
  height: 78px;
  padding: 11px 12px;
  border-radius: 14px;
  color: #fff;
  text-decoration: none;
  overflow: hidden;
  transition: width var(--collapse-t), height var(--collapse-t),
              padding var(--collapse-t), margin var(--collapse-t),
              gap var(--collapse-t), border-radius var(--collapse-t),
              opacity var(--collapse-t),
              transform .16s var(--ease-out), box-shadow .16s var(--ease-out);
}
.promo:hover { transform: translateY(-2px); }
.promo-discord {
  background: linear-gradient(135deg, #5865F2, #7C5CF6);
  box-shadow: 0 4px 14px rgba(88, 101, 242, .28);
}
.promo-discord:hover { box-shadow: 0 8px 20px rgba(88, 101, 242, .4); }
.promo-app {
  background: linear-gradient(135deg, #0F172A, #334155);
  box-shadow: 0 4px 14px rgba(15, 23, 42, .24);
}
.promo-app:hover { box-shadow: 0 8px 20px rgba(15, 23, 42, .34); }
.promo-ic { flex: none; display: grid; place-items: center; width: 30px; height: 30px; margin-top: 1px; border-radius: 9px; background: rgba(255, 255, 255, .16); }
.promo-ic svg { width: 18px; height: 18px; }
.promo-txt { min-width: 0; overflow: hidden; }
.promo-t { font-size: 14px; font-weight: 800; line-height: 1.3; white-space: nowrap; }
.promo-s {
  margin-top: 2px;
  font-size: 11.5px; font-weight: 600; line-height: 1.35;
  color: rgba(255, 255, 255, .84);
  white-space: nowrap;
}
/* The second detail line is supporting copy — quieter than the first, and the
   first thing to go when the rail runs short of room (see .sidebar.snug). */
.promo-s2 {
  margin-top: 0;
  color: rgba(255, 255, 255, .62);
  font-weight: 500;
  height: 16px;
  overflow: hidden;
  transition: opacity var(--collapse-t), height var(--collapse-t),
              margin var(--collapse-t);
}

/* Dismiss — revealed on hover so it never competes with the copy at rest. */
.promo-x {
  position: absolute;
  top: 3px; right: 3px;
  width: 18px; height: 18px;
  display: grid; place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, .16);
  color: #fff;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-fast), background var(--t-fast);
}
.promo-x svg { width: 11px; height: 11px; stroke-width: 2.6; }
.promo:hover .promo-x { opacity: 1; }
.promo-x:hover { background: rgba(255, 255, 255, .34); }
.promo-x:focus-visible { opacity: 1; }

/* Collapsing out after dismissal. The negative margin swallows the grid gap
   that would otherwise linger where the card used to be. */
.promo.gone {
  height: 0; padding-top: 0; padding-bottom: 0;
  margin-bottom: -8px;
  opacity: 0;
  pointer-events: none;
}

/* Held for the few microseconds fitSidebar() spends trialling layouts. Without
   it every trial measures the height the rail is animating away from. */
.sidebar.measuring,
.sidebar.measuring * { transition: none !important; }

/* Stage one on a short window: the promos give up their supporting line and go
   back to the compact two-line card. Worth about thirty pixels, which is the
   difference between fitting and not on a 720px-tall screen. Set by
   fitSidebar() in main.js, which measures rather than guesses a breakpoint. */
.sidebar.snug .promo { height: 56px; padding-top: 9px; padding-bottom: 9px; }
/* Height, not max-width: the line has to stop taking up vertical space, not
   just stop being wide. Clipping it against the shorter card would look the
   same today and break the moment the card's padding changes. */
.sidebar.snug .promo-s2 { height: 0; opacity: 0; margin: 0; }

/* Stage two, last resort: the nav still doesn't fit, so the promos — the only
   optional thing in the rail — step aside entirely.

   This collapses rather than display:none-ing, for the same reason a dismissed
   promo does: fitSidebar runs as part of toggling the rail, and a card
   vanishing in one frame in the middle of that is the single most visible
   stutter in the whole animation. */
.sidebar.tight .promo {
  height: 0;
  padding-top: 0; padding-bottom: 0;
  margin-bottom: -8px;
  opacity: 0;
  pointer-events: none;
}

/* Collapsed, each promo becomes a square icon tile matching the rail rhythm. */
.sidebar.mini .promo {
  width: 40px; height: 40px; padding: 0;
  margin-inline: auto;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
}
.sidebar.mini .promo-ic { margin-top: 0; background: none; }
.sidebar.mini .promo-x { opacity: 0; pointer-events: none; }
.sidebar.mini .promo-txt {
  opacity: 0; max-width: 0; margin: 0;
  transition: opacity calc(var(--collapse-d) * .55) var(--collapse-e),
              max-width var(--collapse-t), margin var(--collapse-t);
}
.promo-txt {
  transition: opacity var(--collapse-t), max-width var(--collapse-t),
              margin var(--collapse-t);
  max-width: 160px;
}

/* ==========================================================================
   Main canvas
   ========================================================================== */

/* The rail's width is animated, but the page beside it deliberately is NOT.

   Animating `margin-left` here meant every frame of the collapse relaid out the
   entire document — on Home that is the carousel, the rank card, the pet SVG
   and every tile, sixty times a second. No amount of reordering the measuring
   pass could fix that, because the animation itself was the layout.

   Instead the margin jumps straight to its final value (one layout, once) and
   the visual gap that opens is closed by a transform, which the compositor can
   run without touching layout at all. toggleMini() sets the inverse offset and
   clears it on the next frame. */
.main {
  flex: 1;
  min-width: 0;
  margin-left: var(--sidebar-w);
  display: flex;
  flex-direction: column;
}
#app.mini .main { margin-left: var(--sidebar-w-collapsed); }
#app.collapsing .main { transition: transform var(--collapse-t); }

/* While the page is offset it briefly overhangs the viewport; clip rather than
   let a horizontal scrollbar flash in and out. */
#app.collapsing { overflow-x: clip; }

.topbar {
  display: none;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  background: #fff;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 50;
}

.view {
  flex: 1;
  padding: 34px 40px 90px;
  max-width: var(--page-max);
  width: 100%;
  margin: 0 auto;
  animation: rise .32s var(--ease) both;
}
.view.flush { max-width: none; padding: 0; }

/* Two-column dashboard layout (Home, Community) */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 22px;
  align-items: start;
}
.split.narrow-right { grid-template-columns: minmax(0, 1fr) 330px; }
/* Leaderboards are a scannable list of names and one number — they do not earn
   an equal share of the page, and at full width the rows stretch until the
   number floats far from the name it belongs to. Capped rather than fluid so
   they stay legible as the window grows. */
.split.boards { grid-template-columns: minmax(0, 1fr) minmax(360px, 440px); }
.split > .col-a, .split > .col-b { display: grid; gap: 22px; align-content: start; }

@media (max-width: 1080px) {
  .split, .split.narrow-right, .split.boards { grid-template-columns: minmax(0, 1fr); }
}

/* ==========================================================================
   Full-screen test session
   The player owns the window while a session is running, so nothing competes
   with the question for attention.
   ========================================================================== */

body.in-session .sidebar,
body.in-session .topbar { display: none; }
body.in-session .main { margin-left: 0 !important; }

/* ==========================================================================
   Mobile
   ========================================================================== */

.scrim-nav {
  position: fixed; inset: 0;
  background: rgba(15, 23, 42, .45);
  z-index: 55;
  animation: fade .18s var(--ease);
}

@media (max-width: 900px) {
  .sidebar {
    transform: translateX(-100%);
    transition: transform var(--t);
    box-shadow: var(--sh-lg);
    width: 264px;
  }
  #app.nav-open .sidebar { transform: none; }
  .main { margin-left: 0 !important; }
  .topbar { display: flex; }
  .view { padding: 22px 16px 80px; }
  .sidebar .collapse-btn { display: none; }
}
@media (min-width: 901px) {
  .scrim-nav { display: none; }
}
