﻿/* ============================================================================
   WEBINOVA DESIGN SYSTEM
   ----------------------------------------------------------------------------
   One stylesheet for the whole site. Before this file existed, every page
   carried its own private copy of the CSS and its own font, which is why the
   site never looked like a single product.

   How to use it:
     <link rel="stylesheet" href="/css/webinova.css">
   and delete that page's inline <style> block.

   How to read it: the sections below run from most general to most specific.
   Change a value in TOKENS and it changes everywhere, on every page. That is
   the entire point - you should almost never need to touch a hex code further
   down this file.
   ========================================================================== */


/* ============================================================================
   1. TOKENS
   The vocabulary of the design. Everything else refers back to these.
   ========================================================================== */

:root {
  /* --- Brand -------------------------------------------------------------
     Maroon is kept deliberately. It reads as academic and institutional,
     which is right for a product sold to lecturers. The old site's mistake
     was using it as large filled areas; here it is an accent on white. */
  --brand:        #800000;
  --brand-deep:   #4d0000;
  --brand-bright: #a52a2a;
  --brand-tint:   #fdf4f4;   /* barely-there wash for hover states */
  --gold:         #ffb703;

  /* --- Ink ---------------------------------------------------------------
     Warm near-black rather than pure #000. Pure black on white vibrates and
     looks cheap at large sizes; a touch of warmth reads as considered. */
  --ink:       #14100f;
  --ink-soft:  #57504d;
  --ink-faint: #8d8681;

  /* --- Paper and lines --------------------------------------------------- */
  --paper:      #ffffff;
  --paper-warm: #faf8f7;   /* alternating section ground */
  --line:       #e8e4e1;
  --line-soft:  #f1eeec;

  /* --- Semantic ---------------------------------------------------------- */
  --ok:      #16a34a;
  --ok-tint: #f0fdf4;
  --warn:    #b45309;
  --info:    #0284c7;

  /* --- Type -------------------------------------------------------------- */
  --font-sans:  'Plus Jakarta Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-serif: 'Instrument Serif', Georgia, 'Times New Roman', serif;

  /* --- Rhythm ------------------------------------------------------------
     A small set of spacing steps. Using these instead of arbitrary pixel
     values is what makes a layout feel deliberate rather than assembled. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;
  --s-8: 64px;
  --s-9: 96px;
  --s-10: 140px;

  --radius:    14px;
  --radius-sm: 10px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  /* --- Elevation ---------------------------------------------------------
     Shadows are soft and low-contrast. Heavy drop shadows are the fastest
     way to make a clean layout look dated. */
  --shadow-sm: 0 1px 2px rgba(20, 16, 15, .04),
               0 2px 8px rgba(20, 16, 15, .04);
  --shadow:    0 2px 4px rgba(20, 16, 15, .03),
               0 12px 32px rgba(20, 16, 15, .07);
  --shadow-lg: 0 4px 8px rgba(20, 16, 15, .04),
               0 24px 64px rgba(20, 16, 15, .10);

  --ease: cubic-bezier(.4, 0, .2, 1);

  /* Height of the fixed header, used for scroll offsets */
  --nav-h: 72px;


  /* --- Legacy aliases ----------------------------------------------------
     The old pages, and some JavaScript that sets colours inline, refer to
     variables by their previous names. Rather than edit working scripts and
     legal copy to chase a rename, the old names are kept here pointing at
     the new palette.

     This is also what makes migrating the remaining pages cheap: link this
     stylesheet, delete the page's inline <style>, and its existing
     var(--maroon) references keep resolving instead of collapsing to an
     invalid colour.

     Do not use these in new markup. */
  --maroon:            var(--brand);
  --maroon-light:      var(--brand-bright);
  --maroon-vibrant:    var(--brand-bright);
  --maroon-dark:       var(--brand-deep);
  --accent-gold:       var(--gold);
  --text-main:         var(--ink);
  --text-muted:        var(--ink-soft);
  --bg-light:          var(--paper-warm);
  --bg-white:          var(--paper);
  --card-bg:           var(--paper);
  --bg-card:           var(--paper);
  --border:            var(--line);
  --blue:              var(--info);
  --light-brown:       #e6ccb2;
  --light-brown-dark:  #b08968;
  --glass:             rgba(255, 255, 255, .8);
  --transition:        all .3s var(--ease);
}

/* Used by the action loader spinner carried over from the old page. */
@keyframes rotation { to { transform: rotate(360deg); } }


/* ============================================================================
   2. RESET AND BASE
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; padding: 0; }
/* Phones paint a translucent blue box over whatever was tapped. It fires on
   every button and link here, including ones that only switch a view, and
   reads as a rendering fault rather than feedback. Our own :active and
   hover states already say a control was pressed. */
* { -webkit-tap-highlight-color: transparent; }

html {
  scroll-behavior: smooth;
  /* So that #anchor links do not hide their heading under the fixed header */
  scroll-padding-top: calc(var(--nav-h) + var(--s-5));
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink);
  background: var(--paper);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a { color: inherit; text-decoration: none; transition: color .2s var(--ease); }
ul, ol { list-style: none; }
img, svg, video { display: block; max-width: 100%; height: auto; }
button, input, textarea, select { font: inherit; color: inherit; }

/* Form controls carry an intrinsic minimum width - an input defaults to
   roughly 20 characters - and inside a flex row they refuse to shrink below
   it. One input beside a button is then enough to push a page wider than a
   phone screen, and every grid on that page lays out against the inflated
   width. This lets them shrink to their container instead. */
input, select, textarea { min-width: 0; }
button { cursor: pointer; background: none; border: none; }

/* Visible focus for keyboard users only. Never remove focus outlines
   outright - it makes the site unusable without a mouse. */
:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
  border-radius: 4px;
}

::selection { background: var(--brand); color: #fff; }


/* --- Scrollbars ---------------------------------------------------------
   The Windows default is a wide grey trough that dominates a white layout.
   Two syntaxes are needed: Firefox understands scrollbar-width and
   scrollbar-color, everything Chromium and WebKit based uses the
   ::-webkit-scrollbar pseudo-elements. macOS overlay scrollbars ignore
   both and stay as they are, which is fine - they are already unobtrusive.

   The thumb keeps a transparent border with background-clip: padding-box.
   That is what makes it read as a slim floating pill inside a wider track,
   instead of a bar wedged against the edge, and it means the hit area stays
   comfortably large even though the visible thumb is thin. */

* {
  scrollbar-width: thin;
  scrollbar-color: #d5cfcb transparent;
}

::-webkit-scrollbar { width: 12px; height: 12px; }

::-webkit-scrollbar-track { background: transparent; }

::-webkit-scrollbar-thumb {
  background: #d5cfcb;
  border: 3.5px solid transparent;
  background-clip: padding-box;
  border-radius: var(--radius-pill);
  transition: background-color .2s var(--ease);
}
::-webkit-scrollbar-thumb:hover {
  background: var(--ink-faint);
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:active { background: var(--ink-soft); background-clip: padding-box; }

/* Removes the little arrow buttons at each end of the track. */
::-webkit-scrollbar-button { display: none; }
::-webkit-scrollbar-corner { background: transparent; }


/* ============================================================================
   3. LAYOUT
   ========================================================================== */

.container {
  width: 100%;
  max-width: 1180px;
  margin-inline: auto;
  padding-inline: var(--s-5);
}

.container-narrow { max-width: 860px; }

/* Vertical breathing room. The reference design's whole character comes from
   generous space, so these are intentionally large. */
.section       { padding-block: var(--s-9); }
.section-tight { padding-block: var(--s-8); }
.section-warm  { background: var(--paper-warm); }

.divider { height: 1px; background: var(--line); border: 0; }


/* ============================================================================
   4. TYPOGRAPHY
   ----------------------------------------------------------------------------
   Sizes use clamp(min, preferred, max) so they scale with the viewport
   without needing a media query for every step.
   ========================================================================== */

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.eyebrow--brand { color: var(--brand); }

/* The big one. Tight tracking and leading is what makes an oversized
   headline read as designed rather than merely large. */
.display {
  font-size: clamp(2.6rem, 6.2vw, 5rem);
  font-weight: 800;
  line-height: 1.03;
  letter-spacing: -.035em;
  text-wrap: balance;
}

.h1 {
  font-size: clamp(2.1rem, 4.4vw, 3.4rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -.03em;
  text-wrap: balance;
}

.h2 {
  font-size: clamp(1.6rem, 3vw, 2.4rem);
  font-weight: 700;
  line-height: 1.18;
  letter-spacing: -.025em;
  text-wrap: balance;
}

.h3 {
  font-size: 1.14rem;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: -.012em;
}

.h4 {
  font-size: .96rem;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: -.005em;
}

/* Serif italic cut into a sans headline. Used sparingly - one phrase per
   page. Overused it stops being an accent and becomes a theme. */
.serif {
  font-family: var(--font-serif);
  font-weight: 400;
  font-style: italic;
  letter-spacing: -.01em;
}

.lede {
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  line-height: 1.7;
  color: var(--ink-soft);
  max-width: 60ch;
}

.muted { color: var(--ink-soft); }
.faint { color: var(--ink-faint); }
.small { font-size: .875rem; }
.tiny  { font-size: .78rem; }

.section-head { max-width: 720px; margin-bottom: var(--s-8); }
.section-head .eyebrow { margin-bottom: var(--s-4); }
.section-head .lede { margin-top: var(--s-4); }


/* ============================================================================
   5. BUTTONS
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: 13px 26px;
  border-radius: var(--radius-pill);
  font-size: .94rem;
  font-weight: 600;
  letter-spacing: -.008em;
  white-space: nowrap;
  border: 1px solid transparent;
  transition: transform .18s var(--ease),
              background-color .18s var(--ease),
              border-color .18s var(--ease),
              color .18s var(--ease),
              box-shadow .18s var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn:disabled,
.btn[disabled] { opacity: .45; cursor: not-allowed; transform: none; }

.btn-primary {
  background: var(--brand);
  color: #fff;
  box-shadow: 0 1px 2px rgba(128, 0, 0, .2), 0 8px 24px rgba(128, 0, 0, .16);
}
.btn-primary:hover:not(:disabled) {
  background: var(--brand-deep);
  box-shadow: 0 2px 4px rgba(128, 0, 0, .22), 0 14px 34px rgba(128, 0, 0, .22);
}

.btn-outline {
  background: transparent;
  color: var(--ink);
  border-color: var(--line);
}
.btn-outline:hover:not(:disabled) {
  border-color: var(--ink);
  background: var(--paper-warm);
}

.btn-ink { background: var(--ink); color: #fff; }
.btn-ink:hover:not(:disabled) { background: #000; }

/* --- Glass -------------------------------------------------------------
   For buttons sitting on the gradient mesh. The instinct is to make a
   button more solid on hover, but over a colourful background that just
   produces a white slab and the glass effect disappears. So this does the
   reverse: on hover the fill gets MORE transparent and the blur goes up,
   so more of the mesh shows through, while a brighter rim and a stronger
   inner highlight keep the edge crisp. The result reads as glass getting
   clearer rather than paint being added.

   backdrop-filter is unsupported on older Firefox; the rgba background
   alone still gives a perfectly readable translucent button there. */
.btn-glass {
  background: rgba(255, 255, 255, .50);
  color: var(--ink);
  border-color: rgba(255, 255, 255, .65);
  backdrop-filter: blur(10px) saturate(170%);
  -webkit-backdrop-filter: blur(10px) saturate(170%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .85),
              0 2px 10px rgba(20, 16, 15, .05);
}
.btn-glass:hover:not(:disabled) {
  background: rgba(255, 255, 255, .26);
  border-color: rgba(255, 255, 255, .95);
  backdrop-filter: blur(18px) saturate(200%);
  -webkit-backdrop-filter: blur(18px) saturate(200%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 1),
              inset 0 -1px 0 rgba(255, 255, 255, .45),
              0 10px 30px rgba(20, 16, 15, .10);
}

/* Same idea inverted, for glass on the dark CTA band. */
.cta-band .btn-glass {
  background: rgba(255, 255, 255, .12);
  color: #fff;
  border-color: rgba(255, 255, 255, .28);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22);
}
.cta-band .btn-glass:hover:not(:disabled) {
  background: rgba(255, 255, 255, .06);
  border-color: rgba(255, 255, 255, .6);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45),
              0 10px 30px rgba(0, 0, 0, .25);
}

.btn-ghost { background: transparent; color: var(--ink-soft); padding-inline: var(--s-4); }
.btn-ghost:hover:not(:disabled) { color: var(--ink); }

.btn-lg    { padding: 16px 34px; font-size: 1rem; }
.btn-sm    { padding: 9px 18px;  font-size: .85rem; }
.btn-block { width: 100%; }

/* Table-row controls. Small enough to sit inside a cell without stretching
   the row, and spaced so two or three fit side by side. */
.btn-xs {
  padding: 5px 10px;
  font-size: .72rem;
  border-radius: 7px;
  letter-spacing: 0;
}
.btn-xs + .btn-xs { margin-left: 5px; }

/* A destructive action that should read as available, not as the primary
   thing to do. Solid red is reserved for buttons that ARE the point of the
   panel; this one lives among neutral siblings. */
.btn-danger-ghost {
  color: #ef4444;
  border-color: rgba(239, 68, 68, .38);
}
.btn-danger-ghost:hover {
  background: rgba(239, 68, 68, .12);
  border-color: rgba(239, 68, 68, .62);
  color: #ef4444;
}


/* ============================================================================
   6. HEADER / NAV
   ----------------------------------------------------------------------------
   The .scrolled class is added by the existing inline script once the page
   scrolls past 50px. Do not rename it.
   ========================================================================== */

/* .site-nav, not a bare `header` element selector. Several app pages use a
   plain <header> for their own top bar, and a global rule would silently
   turn those into a fixed floating nav overlapping the page. Scoping this
   to a class keeps the landing page's nav to the landing page. */
.site-nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 1000;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  background: rgba(255, 255, 255, .72);
  backdrop-filter: saturate(180%) blur(16px);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  border-bottom: 1px solid transparent;
  transition: background .3s var(--ease), border-color .3s var(--ease);
}
.site-nav.scrolled {
  background: rgba(255, 255, 255, .92);
  border-bottom-color: var(--line);
}

.nav-container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
}

.logo {
  font-size: 1.18rem;
  font-weight: 800;
  letter-spacing: -.04em;
  color: var(--ink);
}
.logo span { color: var(--brand); }

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--s-6);
  font-size: .92rem;
  font-weight: 500;
  color: var(--ink-soft);
}
.nav-links a { position: relative; padding-block: 4px; }
.nav-links a:hover { color: var(--ink); }
/* Underline that grows from the left on hover */
.nav-links a::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 0; height: 1.5px;
  background: var(--brand);
  transition: width .25s var(--ease);
}
.nav-links a:hover::after { width: 100%; }

.nav-actions { display: flex; align-items: center; gap: var(--s-3); }

@media (max-width: 860px) {
  .nav-links { display: none; }
}


/* ============================================================================
   7. HERO
   ========================================================================== */

.hero {
  position: relative;
  padding-top: calc(var(--nav-h) + var(--s-9));
  padding-bottom: var(--s-8);
  text-align: center;
  overflow: hidden;
}

.hero-inner {
  position: relative;
  z-index: 2;
  max-width: 880px;
  margin-inline: auto;
}

.hero .display { margin-block: var(--s-5) var(--s-5); }
.hero .lede { margin-inline: auto; text-align: center; }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3);
  margin-top: var(--s-7);
}

.hero-note {
  margin-top: var(--s-5);
  font-size: .82rem;
  color: var(--ink-faint);
}

/* --- Rotating headline phrase ------------------------------------------
   All variants live in the HTML; only visibility changes. They are stacked
   in a single inline-grid cell so the box is as wide and tall as the
   longest variant and nothing reflows when the text swaps - a rotator that
   resizes the headline is far more distracting than the text change itself.

   Non-current items are hidden from assistive technology by the script
   (aria-hidden) so a screen reader is not read three competing headlines. */
.rotator {
  display: inline-grid;
  vertical-align: top;
  text-align: left;
}
.rot-item {
  grid-area: 1 / 1;
  opacity: 0;
  transform: translateY(.28em);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
  pointer-events: none;
}
.rot-item.is-current {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* Centred hero, so centre the stacked phrases too. */
.hero .rotator { text-align: center; justify-items: center; }

/* No motion preference: show the first variant only, permanently. The
   script also stops rotating, so this is belt and braces. */
@media (prefers-reduced-motion: reduce) {
  .rot-item { transition: none; }
}

/* --- The gradient mesh -------------------------------------------------
   This is the one place colour is allowed to be loud. It sits behind the
   text and does the emotional work, which lets the interface itself stay
   black-on-white and calm. Built from overlapping radial gradients so it
   costs no image download. */
.mesh {
  position: absolute;
  inset: auto 0 0 0;
  height: 620px;
  z-index: 0;
  pointer-events: none;
  filter: blur(56px);
  opacity: .82;
  background:
    radial-gradient(46% 46% at 18% 82%, rgba(255, 183, 3,  .55), transparent 68%),
    radial-gradient(50% 50% at 50% 96%, rgba(165, 42, 42,  .48), transparent 66%),
    radial-gradient(42% 42% at 82% 78%, rgba(128, 0,  0,   .40), transparent 68%),
    radial-gradient(38% 38% at 66% 100%, rgba(255, 145, 90, .40), transparent 70%);
}
/* Fades the mesh into the page so it has no hard edge */
.mesh::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--paper) 0%, transparent 42%);
}


/* ============================================================================
   8. STAT / TRUST STRIP
   ========================================================================== */

.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: var(--s-6);
  padding-block: var(--s-7);
  border-block: 1px solid var(--line);
}
.stat-num {
  font-size: clamp(1.8rem, 3.4vw, 2.6rem);
  font-weight: 800;
  letter-spacing: -.035em;
  line-height: 1;
}
.stat-label {
  margin-top: var(--s-2);
  font-size: .85rem;
  color: var(--ink-faint);
}


/* ============================================================================
   9. NUMBERED FEATURE LIST
   The 01 / 02 / 03 pattern from the reference.
   ========================================================================== */

.split {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: var(--s-9);
  align-items: start;
}
@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; gap: var(--s-7); }
}

.numbered { display: flex; flex-direction: column; }

.numbered-item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-5);
  padding: var(--s-5) 0 var(--s-5) var(--s-5);
  border-left: 2px solid var(--line);
  transition: border-color .25s var(--ease);
}
.numbered-item:hover { border-left-color: var(--brand); }

.numbered-num {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 1.05rem;
  color: var(--brand);
  line-height: 1.5;
}
.numbered-item h3 { margin-bottom: var(--s-2); }
.numbered-item p { color: var(--ink-soft); font-size: .93rem; }


/* ============================================================================
   10. APP STORE
   Class names match what filterStoreApps() in the page script expects.
   ========================================================================== */

.store-search {
  position: relative;
  max-width: 440px;
  margin-bottom: var(--s-7);
}
.store-search i {
  position: absolute;
  left: 18px; top: 50%;
  transform: translateY(-50%);
  color: var(--ink-faint);
  font-size: .9rem;
  pointer-events: none;
}
.store-search input {
  width: 100%;
  padding: 14px 18px 14px 46px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--paper);
  font-size: .94rem;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.store-search input::placeholder { color: var(--ink-faint); }
.store-search input:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 4px var(--brand-tint);
}

.store-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: var(--s-5);
  align-items: stretch;
}

.store-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform .3s var(--ease),
              box-shadow .3s var(--ease),
              border-color .3s var(--ease);
}
.store-card:hover {
  transform: translateY(-4px);
  border-color: transparent;
  box-shadow: var(--shadow-lg);
}

/* The old cards used saturated filled headers. A tinted panel with an inset
   icon keeps the card recognisable while letting the page stay white. */
.store-card-header {
  padding: var(--s-6) var(--s-5) var(--s-5);
  background: var(--paper-warm);
  border-bottom: 1px solid var(--line-soft);
}
.store-card-header h3 { margin-bottom: 2px; }
.store-card-header p { font-size: .84rem; color: var(--ink-faint); }

.app-icon {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  margin-bottom: var(--s-4);
  border-radius: 13px;
  background: var(--paper);
  border: 1px solid var(--line);
  color: var(--brand);
  font-size: 1.1rem;
}

.store-card-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: var(--s-5);
  gap: var(--s-4);
}

.store-card-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .02em;
}
.status-live { background: var(--ok-tint); color: var(--ok); }
.status-beta { background: #fffbeb;       color: var(--warn); }
.status-dev  { background: var(--line-soft); color: var(--ink-faint); }

.feature-list { display: flex; flex-direction: column; gap: var(--s-2); }
.feature-list li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-3);
  font-size: .875rem;
  color: var(--ink-soft);
  line-height: 1.5;
}
.feature-list i { color: var(--brand); font-size: .72rem; margin-top: .45em; }

/* Pushes price and button to the card bottom so cards of differing content
   length still line their buttons up. */
.store-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: auto;
  padding-top: var(--s-4);
  border-top: 1px solid var(--line-soft);
}
.store-price .original {
  font-size: .88rem;
  color: var(--ink-faint);
  text-decoration: line-through;
}
.store-price .current {
  font-size: 1.35rem;
  font-weight: 800;
  letter-spacing: -.03em;
}
.store-price .bonus {
  font-size: .72rem;
  font-weight: 600;
  color: var(--ok);
  background: var(--ok-tint);
  padding: 3px 9px;
  border-radius: var(--radius-pill);
}

.discount-ribbon {
  position: absolute;
  top: var(--s-4); right: var(--s-4);
  z-index: 2;
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: #fff;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .02em;
}

.store-card.coming-soon { background: var(--paper-warm); }
.store-card.coming-soon .app-icon { color: var(--ink-faint); }
.store-card.coming-soon:hover { transform: none; box-shadow: var(--shadow-sm); }


/* ============================================================================
   11. NEWS (rendered from Firestore by loadNews())
   ========================================================================== */

.news-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: var(--s-5);
}
.news-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper);
  transition: box-shadow .3s var(--ease), transform .3s var(--ease);
}
.news-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
.news-card:hover .news-more { color: var(--brand); gap: var(--s-3); }
.news-card-img { aspect-ratio: 16 / 9; object-fit: cover; width: 100%; }

/* Shown instead of an image when a news item has none. The colour comes
   from the item's bgColor field, used as an accent rather than a fill. */
.news-card-accent { height: 5px; }

.news-card-body { padding: var(--s-5); }
.news-card-body h3 {
  margin-block: var(--s-3) var(--s-3);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: -.015em;
}
.news-tag {
  display: inline-block;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: var(--brand-tint);
  color: var(--brand);
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.news-more {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: .84rem;
  font-weight: 600;
  color: var(--ink-faint);
  transition: color .2s var(--ease), gap .2s var(--ease);
}


/* ============================================================================
   12. CTA BAND
   ========================================================================== */

.cta-band {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  padding: var(--s-9) var(--s-6);
  text-align: center;
  background: var(--ink);
  color: #fff;
}
.cta-band .lede { color: rgba(255, 255, 255, .72); margin-inline: auto; }
.cta-band .eyebrow { color: rgba(255, 255, 255, .55); }
.cta-band .mesh { opacity: .5; height: 100%; inset: 0; }
.cta-band .mesh::after { display: none; }
.cta-band > * { position: relative; z-index: 2; }

/* ============================================================================
   HOME PAGE ARTWORK
   Renders prepared by brand/prepare_images.py. The white-backed ones sit in
   the page through mix-blend-mode: multiply, under which white vanishes.
   ========================================================================== */

/* --- Hero horizon ---
   The silk sits under the buttons and fades out at both edges: before it
   reaches the headline, so the words always read on clean paper, and before
   the stat strip, so it has no hard bottom edge. */
.hero--horizon { padding-bottom: clamp(160px, 20vw, 300px); }
.hero-horizon {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  width: 100%;
  height: clamp(260px, 34vw, 520px);
  object-fit: cover;
  object-position: center 58%;
  z-index: 1;
  pointer-events: none;
  opacity: .9;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 52%, #000 84%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 52%, #000 84%, transparent 100%);
  animation: horizonDrift 26s ease-in-out infinite alternate;
}
/* Slow enough to read as the silk breathing, not as something moving. */
@keyframes horizonDrift {
  from { transform: scale(1.05) translateX(-1.6%); }
  to   { transform: scale(1.05) translateX(1.6%); }
}

/* --- For whom ---
   Each card takes the colour of its product, so the three read apart at a
   glance and hint at what they lead to. */
.aud-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: var(--s-5);
  margin-top: var(--s-7);
}
.aud-card {
  --aud: var(--brand);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  background: var(--paper-warm);
  color: inherit;
  text-decoration: none;
}
/* These cards are also .reveal, which sets its own transition and, once
   active, transform: none. Both come later in this file at equal or higher
   specificity, so plain .aud-card rules would lose: the scroll-in fade would
   drop out of the transition list, and the hover lift would never happen.
   Scoping through .aud-grid lifts these above them. */
.aud-grid .aud-card {
  transition: opacity .7s var(--ease), transform .4s var(--ease),
              box-shadow .4s var(--ease), border-color .4s var(--ease);
}
.aud--siswa { --aud: #4f46e5; }
.aud--umum  { --aud: #0f766e; }
.aud-grid .aud-card.active:hover {
  transform: translateY(-6px);
  border-color: color-mix(in srgb, var(--aud) 35%, var(--line));
  box-shadow: var(--shadow-lg);
}
.aud-card img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  padding: var(--s-4) var(--s-4) 0;
  mix-blend-mode: multiply;
  transition: transform .6s var(--ease);
}
.aud-card:hover img { transform: scale(1.04); }
.aud-body {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  flex: 1;
  padding: var(--s-3) var(--s-6) var(--s-6);
}
.aud-role {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--aud);
}
.aud-soon {
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--aud) 12%, #fff);
  font-size: .64rem;
  letter-spacing: .04em;
  text-transform: none;
}
.aud-card h3 { font-size: 1.18rem; line-height: 1.3; letter-spacing: -.02em; }
/* Grows to fill, so the three links line up along the bottom. */
.aud-card p { flex: 1; font-size: .9rem; line-height: 1.65; color: var(--ink-soft); }
.aud-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--s-2);
  font-size: .88rem;
  font-weight: 700;
  color: var(--aud);
}
.aud-link i { transition: transform .3s var(--ease); }
.aud-card:hover .aud-link i { transform: translateX(4px); }

/* --- App store icons --- */
.app-icon-img {
  display: block;
  width: 56px;
  height: 56px;
  margin-bottom: var(--s-4);
  border-radius: 15px;
  box-shadow: 0 6px 16px rgba(20, 16, 15, .12);
  transition: transform .4s var(--ease);
}
.store-card:hover .app-icon-img { transform: translateY(-3px) rotate(-3deg); }

/* --- Rector Bundle: the two app icons overlapped, like a stacked deck --- */
.bundle-icons { display: flex; margin-bottom: var(--s-4); }
.bundle-icons .app-icon-img { margin-bottom: 0; }
.bundle-icons .app-icon-img + .app-icon-img { margin-left: -18px; transform: rotate(6deg); }
.store-card:hover .bundle-icons .app-icon-img + .app-icon-img { transform: translate(6px, -3px) rotate(10deg); }
.store-card--bundle { border-color: var(--ink); }
.store-card--bundle .store-card-header {
  background: linear-gradient(135deg, var(--brand-tint), #fffbeb);
}

/* --- Hero note over the silk ---
   The note sits exactly where the horizon image fades in, and as faint grey
   text on colour it disappeared. A frosted pill gives it its own ground. */
.hero--horizon .hero-note {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 7px 16px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .78);
  border: 1px solid rgba(255, 255, 255, .95);
  backdrop-filter: blur(10px) saturate(160%);
  -webkit-backdrop-filter: blur(10px) saturate(160%);
  box-shadow: 0 4px 14px rgba(20, 16, 15, .06);
  color: var(--ink-soft);
  font-weight: 500;
}

/* --- Closing band photo ---
   Specificity 0,2,0 so it beats `.cta-band > *`, which would otherwise pull
   the photo back into the flow and push the headline down. */
.cta-band > .cta-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 60%;
}
/* A dark wash between photo and words: the lamp is the brightest thing in
   the picture, and the headline must be brighter still. */
.cta-band.has-photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    radial-gradient(ellipse 70% 80% at 50% 50%, rgba(20, 14, 12, .72), rgba(20, 14, 12, .5) 75%),
    linear-gradient(180deg, rgba(20, 14, 12, .35), rgba(20, 14, 12, .6));
}
.cta-band.has-photo .mesh { opacity: .28; }

@media (prefers-reduced-motion: reduce) {
  .hero-horizon { animation: none; transform: scale(1.05); }
  .aud-grid .aud-card, .aud-card img, .app-icon-img { transition: none; }
}


/* ============================================================================
   13. FOOTER
   ========================================================================== */

footer {
  padding-block: var(--s-8) var(--s-6);
  border-top: 1px solid var(--line);
  background: var(--paper-warm);
}
.footer-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: var(--s-7);
  margin-bottom: var(--s-7);
}
@media (max-width: 720px) {
  .footer-grid { grid-template-columns: 1fr; }
}
.footer-sub {
  margin-top: var(--s-3);
  max-width: 42ch;
  font-size: .9rem;
  color: var(--ink-soft);
}
.footer-socials { display: flex; gap: var(--s-3); margin-top: var(--s-5); }
.footer-socials a {
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--ink-soft);
  background: var(--paper);
  transition: all .2s var(--ease);
}
.footer-socials a:hover {
  color: #fff;
  background: var(--brand);
  border-color: var(--brand);
}
.footer-col h4 {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--s-4);
}
.footer-col li { margin-bottom: var(--s-2); font-size: .9rem; color: var(--ink-soft); }
.footer-col a:hover { color: var(--brand); }
.copyright {
  padding-top: var(--s-5);
  border-top: 1px solid var(--line);
  font-size: .82rem;
  color: var(--ink-faint);
}


/* ============================================================================
   14. MODALS - auth, dialog, T&C gate
   The ids and class names here are referenced by the page script. Restyled,
   never renamed.
   ========================================================================== */

.auth-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: rgba(20, 16, 15, .45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* The page script opens these with classList.add('show'). Both spellings are
   honoured so neither this file nor the scripts have to be kept in step. */
.auth-overlay.show,
.auth-overlay.active { display: flex; }

.auth-box {
  position: relative;
  width: 100%;
  max-width: 430px;
  max-height: 90vh;
  overflow-y: auto;
  padding: var(--s-7) var(--s-6) var(--s-6);
  background: var(--paper);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  animation: modal-in .3s var(--ease);
}
@keyframes modal-in {
  from { opacity: 0; transform: translateY(14px) scale(.98); }
  to   { opacity: 1; transform: none; }
}

.auth-close {
  position: absolute;
  top: var(--s-4); right: var(--s-4);
  display: grid;
  place-items: center;
  width: 32px; height: 32px;
  border-radius: 50%;
  color: var(--ink-faint);
  cursor: pointer;
  transition: all .2s var(--ease);
}
.auth-close:hover { background: var(--line-soft); color: var(--ink); }

.auth-header { text-align: center; margin-bottom: var(--s-6); }
.auth-header > i {
  display: grid;
  place-items: center;
  width: 52px; height: 52px;
  margin: 0 auto var(--s-4);
  border-radius: 15px;
  background: var(--brand-tint);
  color: var(--brand);
  font-size: 1.25rem;
}
.auth-header h3 {
  font-size: 1.35rem;
  font-weight: 800;
  letter-spacing: -.025em;
  margin-bottom: var(--s-2);
}
.auth-header p { font-size: .9rem; color: var(--ink-soft); }

.auth-step { display: none; }
.auth-step.active { display: block; }

/* Role chooser */
.role-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  padding: var(--s-5);
  margin-bottom: var(--s-3);
  text-align: left;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: all .2s var(--ease);
}
.role-card:hover {
  border-color: var(--brand);
  background: var(--brand-tint);
  transform: translateX(3px);
}
.role-card i:first-child { color: var(--brand); font-size: 1.15rem; }
.role-card strong { display: block; font-size: .97rem; }
.role-card span { font-size: .82rem; color: var(--ink-faint); }
.role-card .chev { color: var(--ink-faint); font-size: .8rem; }

/* --- Login / register tabs ---------------------------------------------
   The capsule is a single element that slides, rather than a background
   that switches on whichever button is active. The script only toggles
   .active on the buttons, exactly as before; :has() lets the container
   react to that without any JavaScript change. */
.auth-tabs {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  padding: var(--s-1);
  margin-bottom: var(--s-5);
  background: var(--paper-warm);
  border-radius: var(--radius-pill);
}
.tab-btn {
  position: relative;
  z-index: 1;              /* label sits above the sliding capsule */
  padding: 9px;
  border-radius: var(--radius-pill);
  font-size: .88rem;
  font-weight: 600;
  color: var(--ink-faint);
  transition: color .25s var(--ease);
}
/* Fallback for browsers without :has() - the capsule appears rather than
   slides, which is what it did before. No one is left without a control. */
.tab-btn.active {
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--shadow-sm);
}

@supports selector(:has(*)) {
  /* Hand the background over to the slider. */
  .tab-btn.active { background: transparent; box-shadow: none; color: var(--ink); }

  .auth-tabs::before {
    content: '';
    position: absolute;          /* absolute, so it is not a grid item */
    top: var(--s-1);
    bottom: var(--s-1);
    left: var(--s-1);
    width: calc(50% - var(--s-1));
    border-radius: var(--radius-pill);
    background: var(--paper);
    box-shadow: var(--shadow-sm);
    /* Slight overshoot, so it settles rather than stops dead. */
    transition: transform .38s cubic-bezier(.34, 1.4, .5, 1);
  }
  /* translateX(100%) is exactly the capsule's own width, which is exactly
     one half of the inner track - so it lands on the second tab. */
  .auth-tabs:has(#tabRegister.active)::before { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
  .auth-tabs::before { transition: none; }
}

.form-group { margin-bottom: var(--s-4); }
.form-label {
  display: block;
  margin-bottom: 6px;
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.form-input {
  width: 100%;
  padding: 12px 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  font-size: .92rem;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.form-input::placeholder { color: var(--ink-faint); }
.form-input:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 4px var(--brand-tint);
}

.auth-sep {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--s-4);
  margin-block: var(--s-5);
  font-size: .78rem;
  color: var(--ink-faint);
}
.auth-sep::before,
.auth-sep::after { content: ''; height: 1px; background: var(--line); }

/* --- Terms and conditions gate -----------------------------------------
   The markup for this block is carried over unchanged from the old page
   because it is legal copy. Only its styling is new. The script toggles
   three classes: .checked on the checkbox, .unlocked on the accept button,
   and .gtc-hiding on the overlay. All three are honoured below. */

#globalTcOverlay {
  position: fixed;
  inset: 0;
  z-index: 5000;
  /* display is switched between none and flex by the page script */
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: rgba(20, 16, 15, .5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  animation: modal-in .35s var(--ease);
}
#globalTcOverlay.gtc-hiding { animation: modal-out .4s var(--ease) forwards; }
@keyframes modal-out {
  to { opacity: 0; transform: scale(.98); }
}

.gtc-box {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 640px;
  max-height: 88vh;
  background: var(--paper);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}

.gtc-header { padding: var(--s-6); border-bottom: 1px solid var(--line); }
.gtc-logo-row {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
}
.gtc-logo-icon {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  border-radius: 13px;
  background: var(--brand-tint);
  color: var(--brand);
  font-size: 1.1rem;
  flex-shrink: 0;
}
.gtc-logo-text h2 {
  font-size: 1.2rem;
  font-weight: 800;
  letter-spacing: -.025em;
  line-height: 1.2;
}
.gtc-logo-text p { font-size: .82rem; color: var(--ink-faint); }
.gtc-header-label {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 5px 11px;
  margin-bottom: var(--s-3);
  border-radius: var(--radius-pill);
  background: var(--paper-warm);
  border: 1px solid var(--line);
  font-size: .72rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.gtc-header-desc { font-size: .86rem; line-height: 1.6; color: var(--ink-soft); }
.gtc-header-desc em { font-style: normal; font-weight: 600; color: var(--ink); }

.gtc-body {
  flex: 1;
  overflow-y: auto;
  padding: var(--s-6);
  font-size: .89rem;
  line-height: 1.72;
  color: var(--ink-soft);
}
.gtc-section { margin-bottom: var(--s-6); }
.gtc-section:last-of-type { margin-bottom: var(--s-4); }
.gtc-section-title {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: -.015em;
  color: var(--ink);
}
.gtc-section-title i { color: var(--brand); font-size: .85rem; }
.gtc-section p { margin-bottom: var(--s-3); }
.gtc-section ul { padding-left: var(--s-5); list-style: disc; }
.gtc-section li { margin-bottom: var(--s-2); }
.gtc-section strong { color: var(--ink); font-weight: 600; }
.gtc-note {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-3);
  padding: var(--s-4);
  margin-top: var(--s-3);
  border-radius: var(--radius-sm);
  background: #fffbeb;
  border: 1px solid #fde68a;
  font-size: .84rem;
  color: #78350f;
}
.gtc-note i { color: var(--warn); margin-top: .25em; }

.gtc-progress-row {
  position: sticky;
  bottom: calc(var(--s-6) * -1);
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4) 0;
  background: var(--paper);
}
.gtc-progress-bar-outer {
  flex: 1;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--line);
  overflow: hidden;
}
.gtc-progress-bar-fill {
  width: 0;
  height: 100%;
  background: var(--brand);
  border-radius: var(--radius-pill);
  transition: width .2s linear;
}
.gtc-progress-label { font-size: .74rem; color: var(--ink-faint); white-space: nowrap; }

.gtc-footer {
  padding: var(--s-5) var(--s-6);
  border-top: 1px solid var(--line);
  background: var(--paper-warm);
}
.gtc-check-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
  cursor: pointer;
}
.gtc-checkbox-custom {
  display: grid;
  place-items: center;
  width: 21px; height: 21px;
  margin-top: 2px;
  border: 1.5px solid var(--line);
  border-radius: 6px;
  background: var(--paper);
  color: transparent;
  font-size: .62rem;
  transition: all .2s var(--ease);
  flex-shrink: 0;
}
.gtc-checkbox-custom.checked {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}
.gtc-check-label { font-size: .82rem; line-height: 1.6; color: var(--ink-soft); }
.gtc-check-label strong { color: var(--ink); font-weight: 600; }

/* Dimmed until the checkbox adds .unlocked. This is a class, not the
   disabled attribute, because that is what the existing script toggles. */
.gtc-btn-accept {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  width: 100%;
  padding: 13px;
  border-radius: var(--radius-pill);
  background: var(--ink-faint);
  color: #fff;
  font-size: .93rem;
  font-weight: 600;
  cursor: not-allowed;
  opacity: .55;
  transition: all .25s var(--ease);
}
.gtc-btn-accept.unlocked {
  background: var(--brand);
  cursor: pointer;
  opacity: 1;
  box-shadow: 0 8px 24px rgba(128, 0, 0, .22);
}
.gtc-btn-accept.unlocked:hover { background: var(--brand-deep); }

.gtc-btn-decline {
  width: 100%;
  padding: var(--s-3);
  margin-top: var(--s-2);
  font-size: .82rem;
  color: var(--ink-faint);
  transition: color .2s var(--ease);
}
.gtc-btn-decline:hover { color: var(--ink); }

.gtc-version {
  margin-top: var(--s-4);
  text-align: center;
  font-size: .7rem;
  letter-spacing: .06em;
  color: var(--ink-faint);
}


/* ============================================================================
   15. HELP BUTTON
   ========================================================================== */

.fab {
  position: fixed;
  right: var(--s-5);
  bottom: var(--s-5);
  z-index: 900;
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.fab-label {
  padding: 9px 14px;
  border-radius: var(--radius-pill);
  background: var(--paper);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  font-size: .82rem;
  font-weight: 600;
}
@media (max-width: 640px) { .fab-label { display: none; } }
.fab-btn {
  display: grid;
  place-items: center;
  width: 54px; height: 54px;
  border-radius: 50%;
  background: var(--brand);
  color: #fff;
  font-size: 1.15rem;
  box-shadow: 0 8px 26px rgba(128, 0, 0, .3);
  transition: transform .2s var(--ease);
}
.fab-btn:hover { transform: scale(1.06); }
.fab-close {
  position: absolute;
  top: -4px; right: -4px;
  display: grid;
  place-items: center;
  width: 21px; height: 21px;
  border-radius: 50%;
  background: var(--ink);
  color: #fff;
  font-size: .55rem;
  box-shadow: var(--shadow-sm);
}


/* ============================================================================
   16. LOADER
   ========================================================================== */

.loader-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  background: rgba(255, 255, 255, .9);
  backdrop-filter: blur(5px);
}
.spinner {
  width: 38px; height: 38px;
  border: 3px solid var(--line);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }


/* ============================================================================
   17. SCROLL REVEAL
   The inline script adds .active once an element enters the viewport.
   ========================================================================== */

.reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.reveal.active { opacity: 1; transform: none; }

/* Anyone who has asked their system to reduce motion gets no animation. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
}


/* ============================================================================
   18. PORTAL
   ----------------------------------------------------------------------------
   The signed-in app catalogue. Class names come from portal.html's existing
   markup and from the strings renderCatalog() and loadNews() build, so they
   are restyled here but never renamed.
   ========================================================================== */

/* --- Top bar (portal uses a static bar, not the landing page's floating nav) */
.topbar {
  position: sticky;
  top: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  padding: var(--s-4) var(--s-5);
  background: rgba(255, 255, 255, .88);
  backdrop-filter: saturate(180%) blur(16px);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  border-bottom: 1px solid var(--line);
}
.user-profile { display: flex; align-items: center; gap: var(--s-4); }
.user-info { display: flex; align-items: center; gap: var(--s-3); }
.user-name { font-size: .92rem; font-weight: 600; }
.package-badge {
  padding: 4px 11px;
  border-radius: var(--radius-pill);
  background: var(--brand-tint);
  color: var(--brand);
  font-size: .72rem;
  font-weight: 700;
}
.btn-logout {
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--ink-soft);
  transition: all .2s var(--ease);
}
.btn-logout:hover { background: var(--brand); border-color: var(--brand); color: #fff; }

/* --- Loading gate --- */
#loadingOverlay {
  position: fixed;
  inset: 0;
  z-index: 9998;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--paper);
}
.loader {
  width: 38px; height: 38px;
  border: 3px solid var(--line);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}

/* --- The Webinova loader: the W written by a pen ---
   Every full-page wait (.loader and .spinner) shows the mark being written,
   stroke by stroke, instead of a generic ring. The animation lives inside
   the SVG itself (brand/make_loader.py), so this is only a picture: no
   markup or script changes on any page, and it keeps animating in a tab the
   browser has throttled. Button spinners (fa-spinner, .spin) keep the ring:
   there is no room for a W inside a button. */
.loader,
.spinner {
  width: 58px;
  height: 41px;
  border: 0;
  border-radius: 0;
  background: url("/img/brand/loader.svg") center / contain no-repeat;
  animation: none;
}
body.tobk .loader,
body.tobk .spinner { background-image: url("/img/brand/loader-q.svg"); }

/* --- News carousel (markup built by loadNews) --- */
.hero-news { padding: var(--s-6) var(--s-5) 0; }
.hero-news .swiper {
  max-width: 1180px;
  margin-inline: auto;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.hero-news .swiper-slide {
  position: relative;
  aspect-ratio: 21 / 8;
  min-height: 240px;
  background: var(--ink);
  cursor: pointer;
}
.hero-news .swiper-slide img {
  width: 100%; height: 100%;
  object-fit: cover;
}
.slide-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--s-3);
  padding: var(--s-7) var(--s-6);
  color: #fff;
  background: linear-gradient(to top, rgba(20, 16, 15, .82) 0%, rgba(20, 16, 15, .2) 55%, transparent 100%);
}
.slide-overlay h2 {
  font-size: clamp(1.3rem, 2.6vw, 2rem);
  font-weight: 800;
  letter-spacing: -.025em;
  line-height: 1.15;
  max-width: 22ch;
}
.slide-overlay p { font-size: .88rem; opacity: .85; }
.slide-tag {
  align-self: flex-start;
  padding: 4px 11px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .18);
  backdrop-filter: blur(8px);
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.hero-news .swiper-pagination-bullet { background: #fff; opacity: .5; }
.hero-news .swiper-pagination-bullet-active { opacity: 1; }
.hero-news .swiper-button-next,
.hero-news .swiper-button-prev { color: #fff !important; }
.hero-news .swiper-button-next::after,
.hero-news .swiper-button-prev::after { font-size: 1.2rem; }

/* --- Bento -------------------------------------------------------------
   Greeting on the left, news on the right with the recommended apps
   beneath it. Two rows on the right, one tall cell on the left. */
.bento {
  display: grid;
  grid-template-columns: 1fr 1.25fr;
  grid-template-areas:
    "intro news"
    "intro featured";
  gap: var(--s-5);
  margin-bottom: var(--s-9);
}
/* Grid items default to min-width:auto, so they refuse to shrink below their
   content. Without this the scrolling track pushes its own cell wider than
   the screen and drags the whole page with it. This one line is the
   difference between a track that scrolls and a page that scrolls. */
.bento > * { min-width: 0; }

@media (max-width: 980px) {
  .bento {
    grid-template-columns: 1fr;
    grid-template-areas: "intro" "news" "featured";
  }
}

.bento-intro {
  grid-area: intro;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--s-6) var(--s-5) var(--s-6) 0;
}
.bento-intro .eyebrow { margin-bottom: var(--s-3); }
.bento-intro .lede { margin-top: var(--s-4); }
.bento-intro .btn { align-self: flex-start; }
@media (max-width: 980px) {
  .bento-intro { padding: 0 0 var(--s-4); }
}

/* News tile, and the panel that stands in for it when there is no news. */
.bento-news,
.bento-news-fallback {
  grid-area: news;
  min-height: 260px;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.bento-news { padding: 0; }
.bento-news .swiper { height: 100%; border-radius: var(--radius-lg); }
.bento-news .swiper-slide { aspect-ratio: auto; height: 100%; min-height: 260px; }

.bento-news-fallback {
  position: relative;
  display: none;              /* loadNews() switches this to flex */
  align-items: flex-end;
  background: var(--ink);
  color: #fff;
}
.bento-news-fallback .mesh { inset: 0; height: 100%; opacity: .55; }
.bento-news-fallback .mesh::after { display: none; }
.bento-fallback-inner {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-6);
}
.bento-fallback-inner h2 {
  font-size: clamp(1.2rem, 2.2vw, 1.7rem);
  font-weight: 800;
  letter-spacing: -.025em;
  line-height: 1.2;
  max-width: 20ch;
}
.bento-fallback-inner p { font-size: .88rem; opacity: .8; }

/* Recommended apps row. */
.bento-featured {
  grid-area: featured;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}
.bento-featured-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
}
.track-nav { display: flex; gap: var(--s-2); }
.track-btn {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--ink-soft);
  font-size: .78rem;
  transition: all .2s var(--ease);
}
.track-btn:hover { background: var(--ink); border-color: var(--ink); color: #fff; }

/* Two tiles fit; a third and beyond scroll. Snap points keep them aligned
   however the row is scrolled, by arrow, wheel or finger. */
.bento-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--s-5);
  min-width: 0;
}
.bento-track.is-scrollable {
  /* min() so a tile can never be wider than the viewport it sits in. On a
     narrow phone the tiles become 78% wide, which also leaves the next one
     peeking in - the clearest possible signal that the row scrolls. */
  grid-auto-columns: minmax(min(240px, 78%), 1fr);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;                 /* the arrows are the affordance */
  padding-bottom: var(--s-2);
}
.bento-track.is-scrollable::-webkit-scrollbar { display: none; }
.bento-track > * { scroll-snap-align: start; }

/* Compact card for the bento. Same component, less of it. */
.bento-tile { min-height: 0; }
.bento-tile .card-desc { display: none; }
.bento-tile .card-label { display: none; }
.bento-tile .card-icon { width: 40px; height: 40px; margin-bottom: var(--s-3); font-size: 1rem; }
.bento-tile .card-title { font-size: 1rem; }
.bento-tile .card-btn { padding: 9px; font-size: .8rem; }
.bento-tile .deco-1 { width: 170px; height: 170px; top: -60px; right: -46px; opacity: .68; }
.bento-tile .deco-2 { width: 120px; height: 120px; top: 26px; right: 16px; opacity: .6; }
/* Span is a catalogue-grid concept; tiles in the bento are always peers. */
.bento-tile.span-2 { grid-column: auto; }

/* --- Section heading inside the portal --- */
.section-title {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -.02em;
}
.section-title i { color: var(--brand); font-size: .95rem; }
.section-title .count {
  margin-left: auto;
  font-size: .78rem;
  font-weight: 600;
  color: var(--ink-faint);
}

/* --- Catalogue grid ---
   Cards were solid saturated blocks. They are white now, with the old
   colour classes reduced to an accent used by the icon and the top rule,
   so renderCatalog can keep emitting the same class names. */
.catalog-grid {
  display: grid;
  /* min(268px, 100%) rather than a bare 268px: a fixed minimum cannot shrink,
     so on a screen narrower than the track the grid overflows its container.
     This is the single most common cause of horizontal scroll on phones. */
  grid-template-columns: repeat(auto-fill, minmax(min(268px, 100%), 1fr));
  /* dense: a wide tile that cannot fit the current row would otherwise
     leave a hole. Dense lets a later narrow tile backfill it. */
  grid-auto-flow: dense;
  gap: var(--s-5);
  margin-bottom: var(--s-8);
}

.catalog-card {
  --accent: var(--brand);
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--s-5);
  min-height: 232px;
  padding: var(--s-5);
  overflow: hidden;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  cursor: pointer;
  opacity: 0;
  animation: card-in .5s var(--ease) forwards;
  transition: transform .3s var(--ease), box-shadow .3s var(--ease), border-color .3s var(--ease);
}
@keyframes card-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
.catalog-card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--accent);
  opacity: 0;
  transition: opacity .3s var(--ease);
}
.catalog-card:hover {
  transform: translateY(-4px);
  border-color: transparent;
  box-shadow: var(--shadow-lg);
}
.catalog-card:hover::before { opacity: 1; }

.card-icon {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  margin-bottom: var(--s-4);
  border-radius: 13px;
  background: var(--paper-warm);
  border: 1px solid var(--line);
  color: var(--accent);
  font-size: 1.1rem;
}
.card-label {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.card-title {
  margin-block: var(--s-2) 6px;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -.02em;
}
.card-desc { font-size: .86rem; line-height: 1.55; color: var(--ink-soft); }

.card-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  width: 100%;
  padding: 11px;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-size: .86rem;
  font-weight: 600;
  transition: background .2s var(--ease);
}
.card-btn:hover { background: var(--accent); }

.card-badge {
  position: absolute;
  top: var(--s-4); right: var(--s-4);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: .68rem;
  font-weight: 700;
}
.badge-live { background: var(--ok-tint); color: var(--ok); }
.badge-beta { background: #fffbeb;        color: var(--warn); }
.badge-dev  { background: var(--line-soft); color: var(--ink-faint); }

.locked-card { background: var(--paper-warm); }
.locked-card:hover { transform: none; box-shadow: var(--shadow-sm); }
.locked-card .card-icon { color: var(--ink-faint); }


/* --- Card artwork -------------------------------------------------------
   renderCatalog() already emits two .deco-circle elements on every card.
   They were hidden while the cards were flat. They are now the card's
   artwork: two blurred radial blooms in the app's own two hues, which is
   what gives each tile its own character without a single image file.

   Everything else in the card is lifted above them, and the blooms sit in
   the top-right corner where no text runs, so contrast is never at risk. */
.catalog-card > *:not(.deco-circle) { position: relative; z-index: 1; }
/* That selector scores 0,2,0 and so outranks .card-badge at 0,1,0, which
   silently turned the absolutely positioned badges into in-flow elements.
   Restored here at equal specificity, after it, so these win. */
.catalog-card > .card-badge { position: absolute; }

.deco-circle {
  position: absolute;
  z-index: 0;
  border-radius: 50%;
  pointer-events: none;
  filter: blur(34px);
  opacity: .55;
  transition: transform .7s var(--ease), opacity .45s var(--ease);
}
.deco-1 {
  width: 200px; height: 200px;
  top: -72px; right: -56px;
  background: radial-gradient(circle, var(--accent) 0%, transparent 68%);
}
.deco-2 {
  width: 150px; height: 150px;
  top: 34px; right: 22px;
  background: radial-gradient(circle, var(--accent-2) 0%, transparent 66%);
}
/* The blooms drift and intensify on hover. This is the whole animation -
   subtle enough to survive being looked at every single day. */
.catalog-card:hover .deco-1 { transform: translate(-16px, 18px) scale(1.14); opacity: .72; }
.catalog-card:hover .deco-2 { transform: translate(-26px, -10px) scale(1.1); opacity: .7; }

/* Coming-soon cards keep the artwork, drained of colour, so the grid still
   has rhythm but the eye is not pulled to something unbuyable. */
.locked-card .deco-circle { filter: blur(34px) grayscale(1); opacity: .32; }

@media (prefers-reduced-motion: reduce) {
  .catalog-card:hover .deco-1,
  .catalog-card:hover .deco-2 { transform: none; }
}

/* Two hues per app: the accent, and a lighter partner it blooms into.
   The pairing is what reads as iridescent rather than as a coloured smudge. */
.card-maroon { --accent: var(--brand); --accent-2: #ff9147; }
.card-green  { --accent: #15803d;      --accent-2: #a3e635; }
.card-blue   { --accent: var(--info);  --accent-2: #7dd3fc; }
.card-gold   { --accent: #b45309;      --accent-2: #fbbf24; }
.card-coral  { --accent: #c2410c;      --accent-2: #fb7185; }
.card-purple { --accent: #6d28d9;      --accent-2: #c084fc; }
.card-dark   { --accent: var(--ink);   --accent-2: #a8a29e; }
.card-light  { --accent: var(--ink-soft); --accent-2: #d6d3d1; }

/* --- Bento rhythm -------------------------------------------------------
   APP_CATALOG already carries a span field (docugen, kurikulumgen,
   webinova_money and gradebook are span-2) that had never been styled.
   Honouring it gives the grid varied tile sizes straight from existing data. */
@media (min-width: 700px) {
  .span-2 { grid-column: span 2; }
  .span-2 .card-desc { max-width: 46ch; }
  /* A wide tile deserves a wider bloom, or the artwork looks stranded. */
  .span-2 .deco-1 { width: 300px; height: 300px; top: -104px; right: -70px; }
  .span-2 .deco-2 { width: 210px; height: 210px; top: 40px; right: 46px; }
}


/* --- Features bar --- */
.features-bar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: var(--s-5);
  padding: var(--s-7) 0;
  border-top: 1px solid var(--line);
}
.feature-item i {
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  margin-bottom: var(--s-3);
  border-radius: 12px;
  background: var(--brand-tint);
  color: var(--brand);
  font-size: .95rem;
}
.feature-item h4 { font-size: .95rem; font-weight: 700; margin-bottom: 3px; }
.feature-item p { font-size: .84rem; color: var(--ink-soft); }

/* --- Portal modals --- */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 2500;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: rgba(20, 16, 15, .45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* portal.html sets style.display; dashboard.html adds .show. Without this
   rule DocuGen's dialog opened invisibly, so every confirmation ("Lanjutkan",
   "Buat RPS Baru") and every error message appeared to do nothing at all.
   Both spellings are honoured so no page has to be kept in step with this
   file. */
.modal-overlay.show,
.modal-overlay.active { display: flex; }

.dialog-card {
  width: 100%;
  max-width: 400px;
  padding: var(--s-6);
  text-align: center;
  background: var(--paper);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  animation: modal-in .3s var(--ease);
}
.dialog-header {
  margin-bottom: var(--s-4);
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: -.02em;
}
.dialog-header i { color: var(--brand); margin-right: var(--s-2); }
.dialog-body p { font-size: .92rem; color: var(--ink-soft); }
.btn-ok, .btn-cancel {
  padding: 11px 22px;
  border-radius: var(--radius-pill);
  font-size: .9rem;
  font-weight: 600;
  width: 100%;
  transition: all .2s var(--ease);
}
.btn-ok { background: var(--brand); color: #fff; }
.btn-ok:hover { background: var(--brand-deep); }
.btn-cancel { background: transparent; border: 1px solid var(--line); color: var(--ink-soft); }
.btn-cancel:hover { border-color: var(--ink); color: var(--ink); }

.app-detail-card {
  display: flex;
  flex-direction: column;
  width: 100%;
  /* Wide enough for a screenshot to be legible. At the old 520px a 16:10
     capture came out around 300px tall, which shows that an app exists but
     not what it is like to use. */
  max-width: 620px;
  max-height: 90vh;
  background: var(--paper);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  animation: modal-in .3s var(--ease);
}
.app-detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  border-bottom: 1px solid var(--line);
}
.app-info-left { display: flex; align-items: center; gap: var(--s-4); }
.app-detail-icon {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: 13px;
  background: var(--brand-tint);
  color: var(--brand);
  font-size: 1.1rem;
  flex-shrink: 0;
}
.app-detail-title-group h3 {
  font-size: 1.1rem;
  font-weight: 800;
  letter-spacing: -.02em;
}
.detail-category { font-size: .78rem; color: var(--ink-faint); }
.btn-close-modal {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  color: var(--ink-faint);
  flex-shrink: 0;
  transition: all .2s var(--ease);
}
.btn-close-modal:hover { background: var(--line-soft); color: var(--ink); }
.app-detail-scroll-area { flex: 1; overflow-y: auto; }
.app-detail-body { padding: var(--s-6); }
.detail-section-title {
  margin-bottom: var(--s-3);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.app-detail-body p { font-size: .92rem; line-height: 1.7; color: var(--ink-soft); }
.app-detail-action-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  border-top: 1px solid var(--line);
  background: var(--paper-warm);
}
.price-container {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-3);
}
.price-original { font-size: .85rem; color: var(--ink-faint); text-decoration: line-through; }
.price-current {
  font-size: 1.3rem;
  font-weight: 800;
  letter-spacing: -.03em;
  /* An amount must never break across lines. "Rp" on one line and "100.000"
     on the next is unreadable at a glance and looks broken. */
  white-space: nowrap;
}

/* Below this width the price and the button cannot share a row: the button
   label is long and does not wrap, so it simply ran off the card. Stack
   them and let the button take the full width. */
@media (max-width: 560px) {
  .app-detail-action-bar {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-4);
  }
  #detailActionBtnContainer { width: 100%; }
  /* showAppDetail builds this wrapper with inline styles, so overriding it
     from the stylesheet needs !important. */
  #detailActionBtnContainer > div {
    align-items: stretch !important;
    width: 100%;
  }
  .btn-buy-now,
  .btn-notify { width: 100%; }
  #detailActionBtnContainer a { justify-content: center; }
}

/* The action buttons inside the detail modal are built by showAppDetail()
   as HTML strings, so they only ever exist at runtime and are easy to miss
   when restyling from the markup alone. */
.btn-buy-now, .btn-notify {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: 12px 22px;
  border-radius: var(--radius-pill);
  font-size: .9rem;
  font-weight: 600;
  white-space: nowrap;
  transition: all .2s var(--ease);
}
.btn-buy-now {
  background: var(--brand);
  color: #fff;
  box-shadow: 0 6px 18px rgba(128, 0, 0, .2);
}
.btn-buy-now:hover { background: var(--brand-deep); }
.btn-notify {
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid var(--line);
}
.btn-notify:hover { border-color: var(--ink); color: var(--ink); }

/* The popup's action column. Replaces inline styles that showAppDetail used
   to write, which is why the mobile rule above needed !important. */
.detail-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-end;
}
.btn-buy-now { text-decoration: none; }     /* also rendered as an <a> now */
.btn-demo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 9px 18px;
  border: 1.5px solid var(--brand);
  border-radius: var(--radius-pill);
  color: var(--brand);
  font-size: .82rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background .2s var(--ease);
}
.btn-demo:hover { background: var(--brand-tint, rgba(128, 0, 0, .06)); }

/* Rector Bundle offer under the buy button in the app popup. */
.bundle-offer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-top: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px dashed var(--brand);
  border-radius: var(--radius);
  background: var(--brand-tint);
  font-size: .82rem;
}
.bundle-offer strong { display: block; font-size: .88rem; }
.bundle-offer span { color: var(--ink-soft); }
.bundle-offer s { color: var(--ink-faint); }
.bundle-offer .btn-demo { flex-shrink: 0; }
@media (max-width: 560px) {
  .detail-actions { align-items: stretch; width: 100%; }
  .btn-demo { width: 100%; }
}

/* --- App popup gallery ---
   Native horizontal scroll with snap, so swiping on a phone works with no
   script at all; the arrows and dots only drive that same scroll. */
.detail-gallery {
  position: relative;
  background: var(--paper-2, #f4f1ee);
  border-bottom: 1px solid var(--line);
}
.detail-gallery[hidden] { display: none; }
.dg-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.dg-track::-webkit-scrollbar { display: none; }
.dg-slide {
  flex: 0 0 100%;
  margin: 0;
  scroll-snap-align: start;
  position: relative;
}
.dg-slide img {
  display: block;
  width: 100%;
  /* A fixed frame, so switching between a tall capture and a wide one does
     not make the popup jump in height under the reader's thumb. */
  aspect-ratio: 16 / 10;
  object-fit: contain;
  background: #fff;
  cursor: zoom-in;
}
.dg-slide figcaption {
  padding: 10px 18px 26px;
  font-size: .82rem;
  color: var(--ink-soft);
  text-align: center;
}
.dg-nav {
  position: absolute;
  top: calc(50% - 30px);
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  color: var(--ink);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .14);
  cursor: pointer;
  transition: transform .2s var(--ease);
}
.dg-nav:hover { transform: scale(1.08); }
.dg-prev { left: 12px; }
.dg-next { right: 12px; }
.dg-dots {
  position: absolute;
  left: 0; right: 0;
  bottom: 8px;
  display: flex;
  justify-content: center;
  gap: 6px;
}
.dg-dots button {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, .2);
  cursor: pointer;
  transition: width .25s var(--ease), background .25s;
}
.dg-dots button.active { width: 20px; border-radius: 4px; background: var(--brand); }
/* One screenshot: nothing to navigate, so no controls promising otherwise. */
.detail-gallery.single .dg-nav,
.detail-gallery.single .dg-dots { display: none; }
.detail-gallery.single .dg-slide figcaption { padding-bottom: 12px; }
@media (hover: none) {
  .dg-nav { display: none; }          /* phones swipe; arrows just cover the picture */
}

.dg-zoom {
  position: fixed;
  inset: 0;
  z-index: 6000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(20, 16, 15, .88);
  cursor: zoom-out;
}
.dg-zoom img {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--radius);
  box-shadow: 0 24px 70px rgba(0, 0, 0, .5);
}

/* --- Fixed terms button --- */
#gtcFixedBtn {
  position: fixed;
  left: var(--s-5); bottom: var(--s-5);
  z-index: 890;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 9px 15px;
  border-radius: var(--radius-pill);
  background: var(--paper);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  font-size: .8rem;
  font-weight: 600;
  color: var(--ink-soft);
  transition: all .2s var(--ease);
}
#gtcFixedBtn:hover { color: var(--brand); border-color: var(--brand); }

/* This used to be hidden below 640px to avoid two floating controls on a
   small screen. Hiding a legal notice on the size most people actually use
   was the wrong trade. It collapses to an icon instead, so it is still
   there and still reachable, without the label crowding the help button. */
@media (max-width: 640px) {
  #gtcFixedBtn {
    width: 42px;
    height: 42px;
    padding: 0;
    justify-content: center;
    border-radius: 50%;
  }
  #gtcFixedBtn .gtc-label { display: none; }
}

/* --- Portal terms modal (markup preserved from the old page) --- */
#portalTcModal {
  position: fixed;
  inset: 0;
  z-index: 5000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: rgba(20, 16, 15, .5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
/* ptcOpen() adds .show. This rule was missing, so the Syarat & Ketentuan
   button and footer link fired, added the class, and changed nothing - the
   same class-mismatch that once kept the login modal shut. */
#portalTcModal.show { display: flex; }
.ptc-box {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 640px;
  max-height: 88vh;
  background: var(--paper);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  animation: modal-in .3s var(--ease);
}
.ptc-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  border-bottom: 1px solid var(--line);
}
.ptc-header-left { display: flex; align-items: center; gap: var(--s-4); }
.ptc-icon {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  border-radius: 13px;
  background: var(--brand-tint);
  color: var(--brand);
  font-size: 1.05rem;
  flex-shrink: 0;
}
.ptc-title { font-size: 1.15rem; font-weight: 800; letter-spacing: -.025em; line-height: 1.2; }
.ptc-sub { font-size: .8rem; color: var(--ink-faint); }
.ptc-close {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  color: var(--ink-faint);
  flex-shrink: 0;
  transition: all .2s var(--ease);
}
.ptc-close:hover { background: var(--line-soft); color: var(--ink); }
.ptc-body {
  flex: 1;
  overflow-y: auto;
  padding: var(--s-6);
  font-size: .89rem;
  line-height: 1.72;
  color: var(--ink-soft);
}
.ptc-section { margin-bottom: var(--s-6); }
.ptc-section-title {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: -.015em;
  color: var(--ink);
}
.ptc-section-title i { color: var(--brand); font-size: .85rem; }
.ptc-section p { margin-bottom: var(--s-3); }
.ptc-section ul { padding-left: var(--s-5); list-style: disc; }
.ptc-section li { margin-bottom: var(--s-2); }
.ptc-section strong { color: var(--ink); font-weight: 600; }
.ptc-note {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-3);
  padding: var(--s-4);
  margin-top: var(--s-3);
  border-radius: var(--radius-sm);
  background: #fffbeb;
  border: 1px solid #fde68a;
  font-size: .84rem;
  color: #78350f;
}
.ptc-note i { color: var(--warn); margin-top: .25em; }
.ptc-footer {
  padding: var(--s-5) var(--s-6);
  border-top: 1px solid var(--line);
  background: var(--paper-warm);
}
.ptc-footer-note { font-size: .82rem; color: var(--ink-soft); margin-bottom: var(--s-4); }
.ptc-btn-close {
  width: 100%;
  padding: 12px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: #fff;
  font-size: .92rem;
  font-weight: 600;
  transition: background .2s var(--ease);
}
.ptc-btn-close:hover { background: var(--brand-deep); }
.ptc-version {
  margin-top: var(--s-4);
  text-align: center;
  font-size: .7rem;
  letter-spacing: .06em;
  color: var(--ink-faint);
}


/* ============================================================================
   19. APP SHELL  (dashboard.html and any future tool page)
   ----------------------------------------------------------------------------
   A persistent sidebar and top bar around a work area, for pages people sit
   inside for an hour rather than pass through. Class names come from the
   existing dashboard markup and the strings its scripts build, so they are
   restyled but never renamed.

   The sidebar is light. A dark rail reads as "admin panel"; the reference
   this was designed against, and the rest of Webinova, are white.
   ========================================================================== */

.app-shell { display: flex; min-height: 100vh; background: var(--paper-warm); }

/* --- Sidebar --- */
.sidebar {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  width: 264px;
  background: var(--paper);
  border-right: 1px solid var(--line);
  overflow-y: auto;
  transition: transform .3s var(--ease);
}
.sidebar-brand { padding: var(--s-6) var(--s-5) var(--s-4); }
.sidebar-brand h2 {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: 1.1rem;
  font-weight: 800;
  letter-spacing: -.035em;
}
.sidebar-brand h2 i { color: var(--brand); font-size: .95rem; }
.sidebar-brand p { margin-top: 2px; font-size: .76rem; color: var(--ink-faint); }

.sidebar-section-label {
  padding: var(--s-5) var(--s-5) var(--s-2);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.sidebar-nav { padding-inline: var(--s-3); }
.sidebar-nav a {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: 10px var(--s-3);
  margin-bottom: 2px;
  border-radius: var(--radius-sm);
  font-size: .88rem;
  font-weight: 500;
  color: var(--ink-soft);
  transition: background .18s var(--ease), color .18s var(--ease);
}
.sidebar-nav a i { width: 18px; text-align: center; margin-top: .18em; font-size: .85rem; }
.sidebar-nav a small { display: block; margin-left: 0; font-size: .72rem; color: var(--ink-faint); }
.sidebar-nav a:hover { background: var(--paper-warm); color: var(--ink); }
.sidebar-nav a.active {
  background: var(--brand-tint);
  color: var(--brand);
  font-weight: 600;
}
.sidebar-nav a.active i { color: var(--brand); }
/* Rail on the active item, the one place the brand colour is a solid block. */
.sidebar-nav a.active::before {
  content: '';
  position: absolute;
  left: calc(var(--s-3) * -1 + 2px);
  top: 20%; bottom: 20%;
  width: 3px;
  border-radius: var(--radius-pill);
  background: var(--brand);
}

.sidebar-footer {
  margin-top: auto;
  padding: var(--s-4) var(--s-5) var(--s-5);
  border-top: 1px solid var(--line);
}
.sidebar-footer a {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: .86rem;
  font-weight: 500;
  color: var(--ink-soft);
}
.sidebar-footer a:hover { color: var(--brand); }

.sidebar-overlay {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: none;
  background: rgba(20, 16, 15, .4);
  backdrop-filter: blur(3px);
}

/* --- Main column --- */
.main-content {
  flex: 1;
  min-width: 0;
  margin-left: 264px;
  display: flex;
  flex-direction: column;
}

.top-bar {
  position: sticky;
  top: 0;
  z-index: 800;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  padding: var(--s-4) var(--s-6);
  background: rgba(250, 248, 247, .92);
  backdrop-filter: saturate(180%) blur(16px);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  border-bottom: 1px solid var(--line);
  /* Its own stacking context, so nothing in the page can paint into it. */
  isolation: isolate;
}

/*
  Phones get a near-solid bar with no blur, and this is a bug fix rather
  than a taste.

  `position: sticky` together with `backdrop-filter` is the combination that
  smears on mobile: the browser takes a snapshot of what is behind the bar to
  blur it, and while you scroll that snapshot goes stale, so a frame of the
  page from a *different* scroll offset gets painted into the header. It
  looks exactly like two screens overlapping - which is what "Koleksi" ghosted
  across "Selamat datang" was.

  The 8% transparency was letting that stale frame through, and saturate(180%)
  was giving it the olive cast. No blur means no snapshot, and nothing to go
  stale. Desktop keeps the glass, where the compositor keeps up with it.
*/
@media (max-width: 992px) {
  .top-bar {
    background: rgba(250, 248, 247, .98);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}
#mobileToggle {
  display: none;
  place-items: center;
  width: 38px; height: 38px;
  border-radius: var(--radius-sm);
  color: var(--ink-soft);
  cursor: pointer;
}
#mobileToggle:hover { background: var(--line-soft); }

.search-box { position: relative; width: min(340px, 42vw); }
.search-box i {
  position: absolute;
  left: 14px; top: 50%;
  transform: translateY(-50%);
  color: var(--ink-faint);
  font-size: .82rem;
  pointer-events: none;
}
.search-box input {
  width: 100%;
  padding: 10px 14px 10px 38px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--paper);
  font-size: .88rem;
}
.search-box input:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 4px var(--brand-tint);
}

.user-info { display: flex; align-items: center; gap: var(--s-3); }
.user-info .name { font-size: .88rem; font-weight: 600; }
.user-info .avatar {
  display: grid;
  place-items: center;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--brand);
  color: #fff;
  font-size: .82rem;
  font-weight: 700;
  flex-shrink: 0;
}

.page-content { flex: 1; padding: var(--s-6); max-width: 1240px; width: 100%; }

/* Pages swap by .active; only one is ever mounted visually. */
.placeholder-content { display: none; animation: page-in .35s var(--ease); }
.placeholder-content.active { display: block; }
@keyframes page-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.page-header { margin-bottom: var(--s-6); }
.page-header h1 {
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  font-weight: 800;
  letter-spacing: -.03em;
}
.page-header p { margin-top: 4px; font-size: .92rem; color: var(--ink-soft); }

/* --- Cards and stats --- */
.content-card {
  padding: var(--s-6);
  margin-bottom: var(--s-5);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}

.stats-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: var(--s-4);
  margin-bottom: var(--s-5);
}
.stat-card {
  padding: var(--s-5);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: box-shadow .25s var(--ease), transform .25s var(--ease);
}
.stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.stat-icon {
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  margin-bottom: var(--s-4);
  border-radius: 11px;
  background: var(--paper-warm);
  border: 1px solid var(--line);
  font-size: .9rem;
  color: var(--ink-soft);
}
.stat-icon.maroon { color: var(--brand);  background: var(--brand-tint); border-color: transparent; }
.stat-icon.brown  { color: #b45309;       background: #fffbeb;           border-color: transparent; }
.stat-icon.green  { color: var(--ok);     background: var(--ok-tint);    border-color: transparent; }
.stat-card .stat-value {
  font-size: 1.9rem;
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1;
}
.stat-card .stat-label { margin-top: 6px; font-size: .82rem; color: var(--ink-faint); }

/* --- The AI panel: the product's promise, made into a control ---------- */
.ai-hero {
  position: relative;
  overflow: hidden;
  padding: var(--s-7);
  margin-bottom: var(--s-6);
  border-radius: var(--radius-lg);
  background: var(--ink);
  color: #fff;
}
.ai-hero .mesh { inset: 0; height: 100%; opacity: .62; }
.ai-hero .mesh::after { display: none; }
.ai-hero > * { position: relative; z-index: 2; }
.ai-hero h2 {
  font-size: clamp(1.3rem, 2.4vw, 1.85rem);
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 1.15;
  max-width: 22ch;
}
.ai-hero p {
  margin-top: var(--s-3);
  font-size: .92rem;
  color: rgba(255, 255, 255, .74);
  max-width: 56ch;
}
.ai-hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-6);
}
.ai-hero .eyebrow { color: rgba(255, 255, 255, .6); }
.ai-hero .btn-glass { background: rgba(255,255,255,.14); color:#fff; border-color: rgba(255,255,255,.3); }
.ai-hero .btn-glass:hover { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.65); }

/* The file picker sits on the dark panel, so it needs its own treatment
   rather than inheriting the form styles meant for white cards. */
.ai-file {
  flex: 1;
  min-width: 220px;
  padding: 10px 14px;
  border: 1px dashed rgba(255, 255, 255, .35);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .08);
  color: rgba(255, 255, 255, .85);
  font-size: .85rem;
  cursor: pointer;
}
.ai-file::file-selector-button {
  margin-right: var(--s-3);
  padding: 6px 14px;
  border: 0;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .9);
  color: var(--ink);
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  cursor: pointer;
}
.ai-status {
  margin-top: var(--s-4);
  font-size: .88rem;
  font-weight: 500;
  color: rgba(255, 255, 255, .9);
}
.ai-cooldown {
  margin-top: var(--s-3);
  font-size: .84rem;
  font-weight: 600;
  color: #fca5a5;
}

/* --- Quota bundles --- */
.quota-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  background: var(--paper-warm);
  border: 1px solid var(--line);
  font-size: .8rem;
  color: var(--ink-soft);
}
.quota-pill i { color: var(--brand); }

.bundle-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: var(--s-4);
}
.bundle {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--s-5) var(--s-4);
  text-align: center;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: transform .25s var(--ease), box-shadow .25s var(--ease);
}
.bundle:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
/* Only the recommended bundle gets weight, so the choice is obvious. */
.bundle.is-popular { border-color: var(--brand); background: var(--brand-tint); }
.bundle-tag {
  position: absolute;
  top: -9px; left: 50%;
  transform: translateX(-50%);
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
}
.bundle.is-popular .bundle-tag { background: var(--brand); }
.bundle-qty { font-size: 2.1rem; font-weight: 800; letter-spacing: -.04em; line-height: 1; }
.bundle-unit { font-size: .74rem; color: var(--ink-faint); margin-bottom: var(--s-3); }
.bundle-price { font-size: 1.05rem; font-weight: 700; letter-spacing: -.02em; }
.bundle-price s { font-size: .82rem; font-weight: 400; color: var(--ink-faint); margin-right: 4px; }
.bundle-each { font-size: .72rem; color: var(--ink-faint); margin-bottom: var(--s-4); }
.bundle .btn { margin-top: auto; }

/* --- Activity feed --- */
.activity-list { display: flex; flex-direction: column; }
.activity-list > * {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4) 0;
  border-bottom: 1px solid var(--line-soft);
}
.activity-list > *:last-child { border-bottom: 0; }
.activity-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--brand);
  flex-shrink: 0;
}
.activity-text { font-size: .89rem; }
.activity-time { font-size: .78rem; color: var(--ink-faint); white-space: nowrap; }

/* --- Forms --- */
/* Mobile first, deliberately. Written the other way round - multi-column by
   default with a narrow-screen override - a long label or a wide control can
   still coax a second column onto a small screen, and the override has to
   fight for it. Starting stacked means a phone cannot get two columns at
   all: the second column is opt-in, and only above 768px. */
.form-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-4) var(--s-5);
  margin-bottom: var(--s-5);
}
@media (min-width: 768px) {
  .form-grid { grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }
}

/* Some fields carry an inline grid-column: span 2 so they run full width on
   desktop. In a single-column grid that span cannot be satisfied, so the
   browser invents an implicit second column to hold it - and every other
   field then flows into that column. Two inline styles were enough to turn
   the whole block back into two columns on a phone.
   Cleared below the breakpoint. !important because they are inline. */
@media (max-width: 767.98px) {
  .form-grid > * { grid-column: auto !important; }
}
.form-group { margin-bottom: var(--s-4); }
.form-group label {
  display: block;
  margin-bottom: 6px;
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink-soft);
}
/* The dashboard forms use bare inputs rather than a .form-input class. */
.placeholder-content input[type="text"],
.placeholder-content input[type="number"],
.placeholder-content input[type="date"],
.placeholder-content input[type="email"],
.placeholder-content select,
.placeholder-content textarea {
  width: 100%;
  padding: 11px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  font-size: .9rem;
  font-family: inherit;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.placeholder-content input:focus,
.placeholder-content select:focus,
.placeholder-content textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 4px var(--brand-tint);
}
.placeholder-content textarea { min-height: 90px; resize: vertical; }

/* File inputs are not covered by the selectors above and were rendering as
   the raw browser control, which looked like a leftover from another site. */
.placeholder-content input[type="file"] {
  width: 100%;
  padding: 10px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-warm);
  font-size: .85rem;
  color: var(--ink-soft);
  cursor: pointer;
}
.placeholder-content input[type="file"]:hover { border-color: var(--brand); }
.placeholder-content input[type="file"]::file-selector-button {
  margin-right: var(--s-3);
  padding: 7px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--paper);
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  transition: all .2s var(--ease);
}
.placeholder-content input[type="file"]::file-selector-button:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

/* --- Repeatable rows --- */
.dynamic-list { display: flex; flex-direction: column; gap: var(--s-3); margin-bottom: var(--s-3); }
.list-item { display: flex; align-items: center; gap: var(--s-3); }
.list-item input { flex: 1; }
.btn-remove-item {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: var(--radius-sm);
  background: var(--paper-warm);
  border: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: .78rem;
  flex-shrink: 0;
  transition: all .2s var(--ease);
}
.btn-remove-item:hover { background: #fee2e2; border-color: #fca5a5; color: #dc2626; }

.btn-add-item, .btn-add-grade {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 8px 16px;
  margin-bottom: var(--s-5);
  border: 1px dashed var(--line);
  border-radius: var(--radius-pill);
  background: transparent;
  font-size: .84rem;
  font-weight: 600;
  color: var(--ink-soft);
  transition: all .2s var(--ease);
}
.btn-add-item:hover, .btn-add-grade:hover {
  border-color: var(--brand);
  border-style: solid;
  color: var(--brand);
  background: var(--brand-tint);
}

.row-label { font-size: .82rem; font-weight: 600; color: var(--ink-soft); }
.matrix-info {
  padding: var(--s-4);
  margin-bottom: var(--s-4);
  border-radius: var(--radius-sm);
  background: #fffbeb;
  border: 1px solid #fde68a;
  font-size: .84rem;
  color: #78350f;
}

/* --- Editable tables ---
   Wide tables scroll inside their own container so the page never does. */
.editable-table {
  width: 100%;
  margin-bottom: var(--s-5);
  border-collapse: separate;
  border-spacing: 0;
  font-size: .86rem;
}
.editable-table th,
.editable-table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: middle;
}
.editable-table th {
  background: var(--paper-warm);
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}
.editable-table th:first-child { border-top-left-radius: var(--radius-sm); }
.editable-table th:last-child  { border-top-right-radius: var(--radius-sm); }
.editable-table tr:last-child td { border-bottom: 0; }
.editable-table input,
.editable-table textarea,
.editable-table select { border-color: transparent !important; background: transparent !important; padding: 7px 8px !important; }
.editable-table input:focus,
.editable-table textarea:focus,
.editable-table select:focus { border-color: var(--brand) !important; background: var(--paper) !important; }

.table-scroll { overflow-x: auto; margin-bottom: var(--s-5); }

/* --- Button variants used only here --- */
.btn-secondary {
  background: var(--paper-warm);
  color: var(--ink);
  border: 1px solid var(--line);
}
.btn-secondary:hover:not(:disabled) { background: var(--line-soft); border-color: var(--ink-faint); }
.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  padding-top: var(--s-5);
  margin-top: var(--s-5);
  border-top: 1px solid var(--line);
}

/* --- PDF preview modal --- */
.modal {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: rgba(20, 16, 15, .55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.modal.show { display: flex; }
.modal-content {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 980px;
  height: 88vh;
  background: var(--paper);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--line);
  font-weight: 700;
  letter-spacing: -.02em;
}
.close-modal {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  color: var(--ink-faint);
  cursor: pointer;
  transition: all .2s var(--ease);
}
.close-modal:hover { background: var(--line-soft); color: var(--ink); }
.preview-frame { flex: 1; width: 100%; border: 0; background: var(--paper-warm); }

/* dashboard.html opens its modals with .active, not .show. Without this the
   PDF preview never appeared at all, on any device. */
.modal.active { display: flex; }

/* PDF pages drawn by PDF.js, for browsers that cannot show a PDF in an
   iframe (every phone). Pages stack and scroll like a document. */
.pdf-pages {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--s-3);
  background: var(--paper-warm);
}
.pdf-pages canvas {
  display: block;
  width: 100%;
  height: auto;
  margin: 0 auto var(--s-3);
  background: #fff;
  box-shadow: 0 1px 3px rgba(20, 16, 15, .12);
}
.pdf-pages-msg { padding: var(--s-6) var(--s-4); text-align: center; font-size: .9rem; color: var(--ink-soft); }
.pdf-pages-msg a { color: var(--brand); font-weight: 700; }

/* "PDF siap": saving on a phone is its own tap. */
.pdf-ready .pdf-ready-name {
  margin: -8px 0 var(--s-4);
  font-size: .84rem;
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}
.pdf-ready-actions { display: grid; gap: var(--s-2); }
.pdf-ready-note { margin-top: var(--s-3); font-size: .8rem; line-height: 1.5; color: #b45309; }
.pdf-ready-note:empty { display: none; }
.pdf-ready > .btn-ghost { margin-top: var(--s-2); }

@media (max-width: 640px) {
  .modal { padding: 0; align-items: stretch; }
  .modal-content { max-width: none; height: 100%; border-radius: 0; }
  .modal .dialog-card { margin: auto var(--s-4); }
}

/* --- Dialog extras used by the dashboard --- */
.dialog-icon {
  display: grid;
  place-items: center;
  width: 52px; height: 52px;
  margin: 0 auto var(--s-4);
  border-radius: 15px;
  background: var(--brand-tint);
  color: var(--brand);
  font-size: 1.25rem;
}
.dialog-footer { display: flex; gap: var(--s-3); margin-top: var(--s-5); }
.btn-dialog {
  flex: 1;
  padding: 11px;
  border-radius: var(--radius-pill);
  font-size: .88rem;
  font-weight: 600;
  transition: all .2s var(--ease);
}

/* --- KurikulumGen -------------------------------------------------------
   That page names its sidebar links .nav-item rather than relying on the
   .sidebar-nav a selector, and its script finds them by that class, so the
   class stays and inherits the same appearance. */
.sidebar .nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 10px var(--s-3);
  margin-bottom: 2px;
  border-radius: var(--radius-sm);
  font-size: .88rem;
  font-weight: 500;
  color: var(--ink-soft);
  transition: background .18s var(--ease), color .18s var(--ease);
}
.sidebar .nav-item i { width: 18px; text-align: center; font-size: .85rem; }
.sidebar .nav-item:hover { background: var(--paper-warm); color: var(--ink); }
.sidebar .nav-item.active { background: var(--brand-tint); color: var(--brand); font-weight: 600; }
.sidebar .nav-item.active i { color: var(--brand); }
.sidebar .nav-item.active::before {
  content: '';
  position: absolute;
  left: -10px; top: 20%; bottom: 20%;
  width: 3px;
  border-radius: var(--radius-pill);
  background: var(--brand);
}
.sidebar-footer .nav-item { margin: 0; padding-inline: 0; }
.sidebar-footer .nav-item.active::before { display: none; }

/* These four were hidden ONLY by the page's own stylesheet, with no inline
   style, and the script reveals them by setting display:block. Deleting that
   stylesheet without carrying these across would have shown every loader and
   result panel on load - a behaviour change, not a cosmetic one. */
#aiLoader, #aiLoader2 { display: none; margin-top: var(--s-4); font-weight: 600; color: var(--brand); }
#resultSection, #resultTahap2 { display: none; animation: page-in .4s var(--ease); }

.matrix-cb { width: 18px; height: 18px; cursor: pointer; accent-color: var(--brand); }
.center-td { text-align: center !important; }

/* KurikulumGen writes plain <table> elements rather than using a class. */
.page-content table {
  width: 100%;
  margin-top: var(--s-4);
  border-collapse: separate;
  border-spacing: 0;
  font-size: .86rem;
}
.page-content table th,
.page-content table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: middle;
}
.page-content table th {
  background: var(--paper-warm);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.page-content table tr:last-child td { border-bottom: 0; }

/* --- TryOut (Webinova-Q) ------------------------------------------------
   Webinova-Q is the student product, and it already has an identity in the
   portal: it is the blue card. So the accent here is blue rather than the
   maroon used across the lecturer-facing pages. Same system, own voice.

   Scoped to body.tobk so the blue never leaks into the other apps. */
body.tobk { --brand: #4f46e5; --brand-deep: #4338ca; --brand-bright: #818cf8; --brand-tint: #eef2ff; }

/* --- The student surface -------------------------------------------------
   Softer than the lecturer-facing pages on purpose. This is the audience we
   said would respond to a lighter, more colourful interface, and it is also
   the one product here that competes on feel rather than on compliance.

   A tinted gradient ground with white cards floating on it, rather than
   white-on-white with borders. Everything is scoped to body.tobk so none of
   it reaches DocuGen, KurikulumGen or the admin console. */
body.tobk.app-shell {
  background:
    radial-gradient(60% 55% at 12% 0%,  rgba(129, 140, 248, .20), transparent 60%),
    radial-gradient(55% 50% at 92% 8%,  rgba(56, 189, 248, .18),  transparent 62%),
    radial-gradient(70% 60% at 50% 100%, rgba(167, 139, 250, .14), transparent 65%),
    #f7f8fd;
  background-attachment: fixed;
}
body.tobk .sidebar {
  background: rgba(255, 255, 255, .72);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  border-right: 1px solid rgba(255, 255, 255, .9);
}
body.tobk .top-bar {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 0;
}
body.tobk .sidebar-nav a { border-radius: var(--radius-pill); }
body.tobk .sidebar-nav a.active {
  background: var(--brand);
  color: #fff;
  box-shadow: 0 6px 18px rgba(79, 70, 229, .28);
}
body.tobk .sidebar-nav a.active i,
body.tobk .sidebar-nav a.active small { color: rgba(255, 255, 255, .85); }
body.tobk .sidebar-nav a.active::before { display: none; }

/* Cards sit on a tint, so they lift with shadow instead of a border line. */
.soft-card {
  padding: var(--s-6);
  background: rgba(255, 255, 255, .82);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, .9);
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 2px rgba(79, 70, 229, .04), 0 12px 32px rgba(79, 70, 229, .07);
}

/* --- This week's tryout: the one thing the page is for ------------------ */
.week-hero {
  position: relative;
  overflow: hidden;
  padding: var(--s-7);
  border-radius: var(--radius-lg);
  color: #fff;
  background: linear-gradient(125deg, #4f46e5 0%, #6366f1 45%, #38bdf8 100%);
  box-shadow: 0 10px 34px rgba(79, 70, 229, .30);
}
/* Two hero cards sharing one slot, taking turns.

   The inactive card is still in the layout (visibility, not display) so the
   deck keeps the height of the taller one and the page below it does not
   jump every five seconds while somebody is reading it. */
/* A card that shows something settled rather than offering a choice.
   Used for the compulsory TKA subjects: they are listed so a student knows
   what they are sitting, not so they can decline them. */
.mode-card.is-fixed {
  cursor: default;
  opacity: .92;
  border-style: dashed;
}

.hero-deck { position: relative; }
.hero-deck-track { display: grid; }
.hero-slide {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transform: translateX(14px);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.hero-slide.is-on { opacity: 1; visibility: visible; transform: none; }
.hero-dots {
  display: flex;
  justify-content: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
}
.hero-dot {
  width: 28px; height: 5px;
  padding: 0;
  border: 0;
  border-radius: 99px;
  cursor: pointer;
  background: rgba(79, 70, 229, .22);
  transition: background .25s var(--ease), width .25s var(--ease);
}
.hero-dot.is-on { width: 44px; background: #4f46e5; }
.hero-dot:focus-visible { outline: 2px solid #4f46e5; outline-offset: 3px; }

/* TKA wears its own colour so the two cards are told apart at a glance,
   not only by reading them. */
.hero-tka {
  background: linear-gradient(125deg, #0f766e 0%, #14b8a6 48%, #7dd3fc 100%);
  box-shadow: 0 10px 34px rgba(15, 118, 110, .30);
}

@media (prefers-reduced-motion: reduce) {
  .hero-slide { transition: none; transform: none; }
}

.week-hero::after {
  content: '';
  position: absolute;
  top: -110px; right: -80px;
  width: 320px; height: 320px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .30), transparent 68%);
  pointer-events: none;
}
.week-hero > * { position: relative; z-index: 1; }
.week-hero h2 {
  font-size: clamp(1.4rem, 2.6vw, 2rem);
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 1.15;
}
.week-hero .lede { color: rgba(255, 255, 255, .84); }
.week-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-5);
}
.week-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .18);
  backdrop-filter: blur(8px);
  font-size: .82rem;
  font-weight: 600;
  white-space: nowrap;
}
.week-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }
/* 48px of padding on a 360px screen leaves the text in a narrow channel,
   which is what made the card look wrongly inset on a phone. */
@media (max-width: 640px) {
  .week-hero { padding: var(--s-5); }
  .week-hero h2 { font-size: 1.45rem; }
  .week-meta { gap: var(--s-2); margin-top: var(--s-4); }
  .week-chip { padding: 6px 12px; font-size: .78rem; }
  .week-actions { margin-top: var(--s-5); }
  .week-actions .btn { width: 100%; }
}
.btn-on-hero {
  background: #fff;
  color: var(--brand-deep);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .14);
}
.btn-on-hero:hover:not(:disabled) { background: #f1f5ff; }

/* The TKA hero is teal, and this button took its text colour from the
   product brand, which is indigo. A deep indigo label on a white pill on a
   teal card belongs to neither: it reads as a button borrowed from another
   page. It takes the card's own colour instead. */
.hero-tka .btn-on-hero { color: #0f766e; }
.hero-tka .btn-on-hero:hover:not(:disabled) { background: #effcfa; }

/* Glass on the indigo hero.
   .btn-glass defaults to dark text on translucent white, which is right on a
   pale page and wrong here: against the hero it came out as a washed pale
   pill that reads as disabled - exactly the wrong signal for "Pakai tiket",
   which is the cheaper option and the one we would rather people took.
   .cta-band and .ai-hero already carry this inversion; the week hero did
   not. */
.week-hero .btn-glass {
  background: rgba(255, 255, 255, .16);
  color: #fff;
  border-color: rgba(255, 255, 255, .42);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .28);
}
.week-hero .btn-glass:hover:not(:disabled) {
  background: rgba(255, 255, 255, .26);
  border-color: rgba(255, 255, 255, .8);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5),
              0 10px 26px rgba(0, 0, 0, .22);
}

/* --- Pastel stat tiles, one hue each so they read apart at a glance ----- */
.soft-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  gap: var(--s-4);
}
.soft-stat {
  padding: var(--s-5);
  border-radius: var(--radius);
  border: 1px solid rgba(255, 255, 255, .8);
}
.soft-stat .n {
  font-size: 1.8rem;
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1;
}
.soft-stat .l { margin-top: 6px; font-size: .8rem; color: var(--ink-soft); }
/* The hidden attribute must always win. Browsers implement it as a plain
   display:none that any class setting display overrides, so an element can
   carry `hidden` and still show - the same shape of bug as the class
   mismatches that kept the login and terms modals shut. */
[hidden] { display: none !important; }

/* --- Empty state: before there is anything to list --- */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--s-4) var(--s-4) var(--s-2);
}
.empty-state img {
  width: 100%;
  max-width: 220px;
  height: auto;
  margin-bottom: var(--s-3);
  /* The illustration is on white; this lets it sit on a tinted card. */
  mix-blend-mode: multiply;
}
.empty-state p { max-width: 34ch; font-size: .88rem; color: var(--ink-soft); line-height: 1.6; }
.empty-state strong { display: block; margin-bottom: 2px; font-size: .98rem; color: var(--ink); }

/* --- The W mark beside the wordmark ---
   Sized in em so it scales with whatever heading it sits in, and nudged
   below the baseline so the W's feet line up with the letters rather than
   floating above them. width/height attributes on the <img> reserve its
   space before it loads, so the header does not jump.
   Do not place it on maroon: its maroon stroke disappears. */
.wv-mark {
  display: inline-block;
  height: 1.12em;
  width: auto;
  margin-right: .32em;
  vertical-align: -0.2em;
}

/* Direct child only. As `.soft-stat i` this also caught the icon inside any
   button placed in a tile - the cart in "Beli" became a 34px tinted box with
   a bottom margin, knocking the label off-centre. */
.soft-stat > i {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  margin-bottom: var(--s-4);
  border-radius: 10px;
  background: rgba(255, 255, 255, .75);
  font-size: .85rem;
}
.tint-indigo { background: linear-gradient(140deg, #eef2ff, #e0e7ff); }

/* --- Ticket packs ---
   Their own layout rather than borrowed stat tiles. Stat tiles are built for
   a number and a label; a pack has a price, what it buys, and a button - and
   squeezed into a tile the two buttons landed at different heights and the
   cheaper one read as disabled. Here the cards stretch to equal height and
   the button is pushed to the bottom of each, so the two always line up. */
.ticket-packs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr));
  gap: var(--s-5);
}
.ticket-pack {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--s-6) var(--s-5) var(--s-5);
  border-radius: var(--radius);
  background: #fff;
  border: 1px solid var(--line);
  transition: transform .3s var(--ease), box-shadow .3s var(--ease);
}
.ticket-pack:hover { transform: translateY(-3px); box-shadow: 0 16px 36px rgba(79, 70, 229, .1); }
.ticket-pack.featured {
  background: linear-gradient(160deg, #eef2ff, #fff 62%);
  border-color: rgba(79, 70, 229, .35);
  box-shadow: 0 0 0 3px rgba(79, 70, 229, .08);
}
.tp-ribbon {
  position: absolute;
  top: -11px;
  right: var(--s-5);
  padding: 4px 12px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: #fff;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .02em;
  box-shadow: 0 4px 12px rgba(79, 70, 229, .3);
}
.tp-qty {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--brand);
}
.tp-price {
  margin-top: var(--s-2);
  font-size: 2rem;
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1.1;
  color: var(--ink);
}
.tp-per { margin-top: 4px; font-size: .82rem; color: var(--ink-faint); }
.tp-per s { opacity: .7; }
.tp-list {
  list-style: none;
  margin: var(--s-5) 0;
  padding: 0;
  /* Grows to fill, which is what pins both buttons to the same baseline
     however long either list becomes. */
  flex: 1;
}
.tp-list li {
  display: flex;
  gap: 9px;
  align-items: flex-start;
  padding: 5px 0;
  font-size: .88rem;
  color: var(--ink-soft);
}
.tp-list li i { color: var(--brand); font-size: .75rem; margin-top: 5px; }

/* An outline that still reads as a button on a white card. The generic
   .btn-outline is a pale grey line, fine as a secondary action on a tinted
   panel and easy to mistake for disabled on white. */
.btn-outline-brand {
  background: #fff;
  color: var(--brand);
  border: 1.5px solid var(--brand);
}
.btn-outline-brand:hover:not(:disabled) { background: var(--brand-tint); }
.tint-indigo i { color: #4f46e5; }
.tint-sky    { background: linear-gradient(140deg, #f0f9ff, #e0f2fe); }
.tint-sky i  { color: #0284c7; }
.tint-mint   { background: linear-gradient(140deg, #f0fdf4, #dcfce7); }
.tint-mint i { color: #16a34a; }
.tint-peach  { background: linear-gradient(140deg, #fff7ed, #ffedd5); }
.tint-peach i{ color: #ea580c; }
/* --- Webinova-Q calendar: the next seven days ---------------------------
   Seven columns where there is room for them; below that a swipeable row,
   because seven squeezed columns on a phone leave each day too narrow to
   read a time in. */
.cal-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-3) var(--s-5);
  margin-bottom: var(--s-5);
}
.cal-legend { display: flex; flex-wrap: wrap; gap: var(--s-4); font-size: .76rem; color: var(--ink-soft); }
.cal-legend span { display: inline-flex; align-items: center; gap: 6px; }
.cal-dot { width: 10px; height: 10px; border-radius: 3px; }
.cal-dot.is-open { background: var(--brand-tint); border: 1px solid var(--brand-bright); }
.cal-dot.is-mine { background: var(--brand); }
.cal-dot.is-off  { background: var(--line); }

.cal-strip {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--s-2);
}
.cal-day {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  min-width: 0;
  padding: var(--s-3);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, .6);
  border: 1px solid var(--line-soft);
}
.cal-day.is-today { border-color: var(--brand-bright); box-shadow: 0 0 0 3px var(--brand-tint); }
.cal-day.is-rest {
  background: linear-gradient(160deg, rgba(251, 146, 60, .12), rgba(240, 171, 252, .08));
  border-color: rgba(251, 146, 60, .25);
}
.cal-day.is-future { background: rgba(255, 255, 255, .35); }
.cal-day-head { display: flex; flex-direction: column; line-height: 1.25; }
.cal-wd { font-size: .8rem; font-weight: 800; text-transform: capitalize; }
.cal-date { font-size: .72rem; color: var(--ink-faint); }
.cal-slots { display: flex; flex-direction: column; gap: 6px; }
.cal-slot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px;
  width: 100%;
  padding: 7px 9px;
  border-radius: 9px;
  border: 1px solid transparent;
  font: inherit;
  font-size: .82rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: left;
}
.cal-slot small { font-size: .66rem; font-weight: 600; white-space: nowrap; }
.cal-slot.is-open { background: var(--brand-tint); color: var(--brand-deep); border-color: rgba(129, 140, 248, .45); }
button.cal-slot.is-open { cursor: pointer; transition: transform .15s var(--ease), box-shadow .15s var(--ease); }
button.cal-slot.is-open:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(79, 70, 229, .18); }
.cal-slot.is-mine { background: var(--brand); color: #fff; box-shadow: 0 4px 12px rgba(79, 70, 229, .28); }
.cal-slot.is-off  { background: var(--line-soft); color: var(--ink-faint); text-decoration: line-through; }
.cal-slot.is-off small { text-decoration: none; }
.cal-slot.is-est  { background: transparent; color: var(--ink-faint); border: 1px dashed var(--line); font-weight: 700; }
.cal-note { font-size: .72rem; color: var(--ink-faint); line-height: 1.45; }
.cal-note i { color: #ea580c; margin-right: 3px; }

@media (max-width: 1180px) {
  .cal-strip {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 138px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    padding-bottom: var(--s-2);
    margin-inline: calc(var(--s-2) * -1);
    padding-inline: var(--s-2);
  }
  .cal-day { scroll-snap-align: start; }
}

.tint-rose   { background: linear-gradient(140deg, #fff1f2, #ffe4e6); }
.tint-rose i { color: #e11d48; }

/* Small state pill used in lists and the results table. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 11px;
  border-radius: var(--radius-pill);
  font-size: .72rem;
  font-weight: 700;
}
.pill-open   { background: #dcfce7; color: #15803d; }
.pill-wait   { background: #ffedd5; color: #c2410c; }
.pill-done   { background: #e0e7ff; color: #4338ca; }
.pill-closed { background: #f1f5f9; color: #64748b; }

body.tobk .search-box input {
  background: rgba(255, 255, 255, .75);
  border-color: rgba(255, 255, 255, .9);
}

/* Form controls inside the booking modal. Nothing else on this page uses
   .card or .placeholder-content, so without this the selects fall back to
   raw browser dropdowns in the middle of a styled flow. */
.onboard-body input[type="text"],
.onboard-body input[type="number"],
.onboard-body select,
.onboard-body textarea {
  width: 100%;
  padding: 11px 14px;
  margin-bottom: var(--s-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  font-size: .9rem;
  font-family: inherit;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.onboard-body select:focus,
.onboard-body input:focus,
.onboard-body textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 4px var(--brand-tint);
}
.onboard-body label {
  display: block;
  margin-bottom: 6px;
  font-size: .8rem;
  font-weight: 700;
  color: var(--ink-soft);
}
/* The programme rows stack their own controls, so the shared centred
   .choice-item treatment is overridden here. */
/* The prodi rows in the booking modal reuse .choice-item as a container, but
   they are not choices - you pick inside them, from the selects. So they do
   not light up on hover.

   Scoped with :has(select) rather than to the whole modal. It used to cancel
   hover for every .choice-item inside .onboard-body, which caught the sitting
   slots too: those ARE choices, and a clickable card that does nothing under
   the cursor is the single clearest way to make an interface feel dead. */
.onboard-body .choice-item:has(select) {
  text-align: left;
  margin-bottom: var(--s-3);
  cursor: default;
}
.onboard-body .choice-item:has(select):hover {
  border-color: var(--line);
  background: transparent;
}
.onboard-body .choice-item select:last-child { margin-bottom: 0; }

.mode-card h4 {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: .98rem;
  font-weight: 700;
  margin-bottom: var(--s-2);
}
.mode-card h4 i { color: var(--brand); }
.mode-card.active h4 i { color: var(--brand-deep); }

.hamburger-menu {
  display: none;
  place-items: center;
  width: 38px; height: 38px;
  margin-right: var(--s-3);
  border-radius: var(--radius-sm);
  color: var(--ink-soft);
  cursor: pointer;
}
.hamburger-menu:hover { background: var(--line-soft); }

.sidebar-backdrop {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: none;
  background: rgba(20, 16, 15, .4);
  backdrop-filter: blur(3px);
}
.sidebar-backdrop.active { display: block; }
@media (max-width: 992px) { .hamburger-menu { display: grid; } }

.header-title { font-size: 1.05rem; font-weight: 700; letter-spacing: -.02em; }
.header-actions { display: flex; align-items: center; gap: var(--s-4); }
.notification {
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--ink-soft);
  cursor: pointer;
}
.notification:hover { border-color: var(--brand); color: var(--brand); }
.user-profile { display: flex; align-items: center; gap: var(--s-3); }
.user-avatar { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; }
.user-name { font-size: .88rem; font-weight: 600; }

.content { padding: var(--s-6); max-width: 1240px; }
@media (max-width: 640px) { .content { padding: var(--s-5) var(--s-4); } }

.nav-view { display: none; animation: page-in .35s var(--ease); }
.nav-view.active { display: block; }

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: var(--s-4);
  margin-bottom: var(--s-6);
}
/* This page's stat card is icon-beside-text rather than icon-above. */
.stats-grid .stat-card {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--s-4);
}
.stats-grid .stat-icon { margin: 0; background: var(--brand-tint); color: var(--brand); border-color: transparent; }
.stat-info h4 { font-size: .8rem; font-weight: 500; color: var(--ink-faint); }
.stat-info p { font-size: 1.6rem; font-weight: 800; letter-spacing: -.035em; line-height: 1.1; }

.dashboard-sections { display: grid; gap: var(--s-5); }
.card-container {
  padding: var(--s-6);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.btn-view-all {
  margin-left: auto;
  font-size: .82rem;
  font-weight: 600;
  color: var(--brand);
}
.btn-view-all:hover { text-decoration: underline; }

/* --- Tryout cards ---
   A grid of cards rather than a stack of rows, so a student scans sessions
   the way they would scan a course catalogue. Each carries a soft accent
   bloom, the same technique as the portal tiles: no images to download. */
.tryout-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(290px, 100%), 1fr));
  gap: var(--s-4);
}
.tryout-item {
  --accent: var(--brand);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  overflow: hidden;
  padding: var(--s-5);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: transform .28s var(--ease), box-shadow .28s var(--ease), border-color .28s var(--ease);
}
.tryout-item::after {
  content: '';
  position: absolute;
  top: -70px; right: -60px;
  width: 180px; height: 180px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent) 0%, transparent 68%);
  opacity: .16;
  filter: blur(28px);
  pointer-events: none;
  transition: opacity .3s var(--ease), transform .5s var(--ease);
}
.tryout-item:hover {
  transform: translateY(-3px);
  border-color: transparent;
  box-shadow: var(--shadow);
}
.tryout-item:hover::after { opacity: .3; transform: translate(-12px, 12px) scale(1.1); }
.tryout-item > * { position: relative; z-index: 1; }

.to-details h5 {
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.3;
  margin-bottom: var(--s-3);
}
.to-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-4);
  font-size: .82rem;
  color: var(--ink-soft);
}
.to-meta span { display: inline-flex; align-items: center; gap: 6px; }
.to-meta i { color: var(--ink-faint); }
.link-detail {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: 0;
  padding: 0;
  font-size: .82rem;
  font-weight: 600;
  color: var(--brand);
  cursor: pointer;
}
.link-detail:hover { text-decoration: underline; }

.btn-mulai {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  width: 100%;
  margin-top: auto;
  padding: 11px 18px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: #fff;
  font-size: .87rem;
  font-weight: 600;
  transition: background .2s var(--ease);
}
.btn-mulai:hover:not(:disabled) { background: var(--brand-deep); }
.btn-mulai:disabled { background: var(--ink-faint); cursor: not-allowed; }

.to-date {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: var(--brand-tint);
  color: var(--brand);
  font-size: .72rem;
  font-weight: 600;
  white-space: nowrap;
}

.score-badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: var(--line-soft);
  font-size: .72rem;
  font-weight: 700;
}

/* --- Booking modal --- */
.onboard-overlay {
  position: fixed;
  inset: 0;
  z-index: 4000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: rgba(20, 16, 15, .5);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.onboard-overlay.active { display: flex; }
.onboard-card {
  width: 100%;
  max-width: 520px;
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--paper);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  animation: modal-in .3s var(--ease);
}
.onboard-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  border-bottom: 1px solid var(--line);
  font-weight: 700;
  letter-spacing: -.02em;
}
.onboard-close {
  display: grid; place-items: center;
  width: 32px; height: 32px;
  border-radius: 50%;
  color: var(--ink-faint);
  cursor: pointer;
}
.onboard-close:hover { background: var(--line-soft); color: var(--ink); }
.onboard-body { flex: 1; overflow-y: auto; padding: var(--s-6); }
.btn-onboard {
  width: 100%;
  padding: 12px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: #fff;
  font-weight: 600;
  transition: background .2s var(--ease);
}
.btn-onboard:hover { background: var(--brand-deep); }

.step-view { display: none; }
.step-view.active { display: block; }
.step-indicator {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-bottom: var(--s-5);
  font-size: .78rem;
  font-weight: 600;
  color: var(--ink-faint);
}

.mode-grid { display: grid; gap: var(--s-3); }
.mode-card {
  padding: var(--s-5);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  transition: all .2s var(--ease);
}
.mode-card:hover { border-color: var(--brand); background: var(--brand-tint); }
.mode-card.active { border-color: var(--brand); background: var(--brand-tint); box-shadow: 0 0 0 3px var(--brand-tint); }

.choice-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(140px,100%),1fr)); gap: var(--s-3); }
.choice-item {
  padding: var(--s-4);
  text-align: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: all .2s var(--ease);
}
.choice-item:hover, .choice-item.choice-select { border-color: var(--brand); background: var(--brand-tint); }

/* A full slot is not selectable, so it must not respond like one. Lighting
   up under the cursor and then refusing the click is worse than looking
   plainly unavailable. */
.choice-item.is-full {
  opacity: .45;
  cursor: not-allowed;
}
.choice-item.is-full:hover {
  border-color: var(--line);
  background: transparent;
}

/* Result modal on the dashboard. */
.fail-card {
  width: 100%;
  max-width: 600px;
  padding: var(--s-8) var(--s-6);
  text-align: center;
  background: var(--paper);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

@media (max-width: 992px) { .sidebar.mobile-active { transform: none; } }

/* --- Responsive shell ---
   992px, not a rounder number, because toggleSidebar() in dashboard.html
   uses `window.innerWidth <= 992` to decide whether tapping a nav link
   should close the drawer. If the two disagreed there would be a band of
   widths where the sidebar is off-canvas but never closes itself. */
@media (max-width: 992px) {
  .sidebar { transform: translateX(-100%); box-shadow: var(--shadow-lg); }
  .sidebar.active { transform: none; }
  .sidebar-overlay.active { display: block; }
  .main-content { margin-left: 0; }
  #mobileToggle { display: grid; }
}
@media (max-width: 640px) {
  .page-content { padding: var(--s-5) var(--s-4); }
  .top-bar { padding: var(--s-3) var(--s-4); }
  .search-box { width: 100%; }
  .user-info .name { display: none; }

  /* .form-grid is stacked by default now, so it needs no override here. */

  /* Full-width actions, stacked. Half-width buttons side by side leave both
     too small to hit comfortably. */
  .btn-row { flex-direction: column; }
  .btn-row .btn { width: 100%; }

  /* Tables keep the design, just at a size that fits. Headers are allowed
     to wrap here: nowrap headers across many columns are the main reason a
     table forces the page wider than the screen. */
  .editable-table { font-size: .8rem; }
  .editable-table th { white-space: normal; }
  .editable-table th,
  .editable-table td { padding: 8px; }
  /* Last resort for genuinely wide tables: scroll the form, not the page. */
  .placeholder-content form { overflow-x: auto; }
}

/* The Cover page builds its token row with inline styles, so it cannot be
   reached by class. :has() lets the row be found by the control inside it,
   and wrapped, without editing the markup. */
@media (max-width: 560px) {
  div:has(> #inputTokenKurikulum) { flex-wrap: wrap; }
  div:has(> #inputTokenKurikulum) > #inputTokenKurikulum { flex: 1 1 100%; }
  div:has(> #inputTokenKurikulum) > .btn { width: 100%; }
}


/* ============================================================================
   20. ADMIN CONSOLE
   ----------------------------------------------------------------------------
   The admin console is dark. That is not decoration: it is an internal tool
   with destructive controls, and looking unmistakably different from the
   customer-facing site is a safety feature - you should never be unsure
   which one you are typing into.

   It is a re-skin, not a second stylesheet. body.admin re-points the tokens,
   and every component built above - sidebar, cards, tables, buttons, modals,
   forms - inverts with them. This is what the token layer was for.
   ========================================================================== */

/* color-scheme only has effect on the root element. Declared on body it is
   ignored, and the browser keeps painting its own chrome - scrollbar track,
   canvas beyond the content - in light colours, which showed as a pale strip
   down the right edge of an otherwise dark console. :has() lets the root
   react to the class that is actually on body. */
:root:has(body.admin) {
  color-scheme: dark;
  background: #0f0c0a;
}

body.admin {
  --paper:      #171310;   /* cards and raised surfaces */
  --paper-warm: #0f0c0a;   /* page ground */
  --ink:        #f4f1ef;
  --ink-soft:   #a8a29e;
  --ink-faint:  #78716c;
  --line:       #2b2320;
  --line-soft:  #221c19;

  /* Maroon is too dark to read against this ground, so the brand shifts up
     in lightness while keeping its hue. Same identity, legible. */
  --brand:        #e0736b;
  --brand-deep:   #c4544c;
  --brand-bright: #f08d85;
  --brand-tint:   rgba(224, 115, 107, .14);

  --ok:      #4ade80;
  --ok-tint: rgba(74, 222, 128, .13);
  --warn:    #fbbf24;
  --info:    #60a5fa;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow:    0 2px 6px rgba(0, 0, 0, .45), 0 12px 32px rgba(0, 0, 0, .35);
  --shadow-lg: 0 4px 10px rgba(0, 0, 0, .5), 0 24px 64px rgba(0, 0, 0, .45);

  color-scheme: dark;
  background: var(--paper-warm);

  /* The legacy aliases must be re-declared here, not just inherited.
     A custom property whose value contains var() is resolved where it is
     DECLARED, so --text-main: var(--ink) on :root computed against the light
     --ink and inherited down as a fixed hex. Re-pointing --ink on body.admin
     could not retroactively change it, and every legacy variable in the
     admin markup rendered in light-theme colours - black text on a black
     card. Anything added to the alias block above belongs here too. */
  --maroon:            var(--brand);
  --maroon-light:      var(--brand-bright);
  --maroon-vibrant:    var(--brand-bright);
  --maroon-dark:       var(--brand-deep);
  --accent-gold:       var(--gold);
  --text-main:         var(--ink);
  --text-muted:        var(--ink-soft);
  --bg-light:          var(--paper-warm);
  --bg-white:          var(--paper);
  --card-bg:           var(--paper);
  --bg-card:           var(--paper);
  --border:            var(--line);
  --blue:              var(--info);
  --light-brown:       #d6a77a;
  --light-brown-dark:  #c08f5f;
  --glass:             rgba(255, 255, 255, .06);
}

/* Native controls need telling too, or they render as light boxes. */
body.admin input,
body.admin select,
body.admin textarea {
  background: var(--paper-warm);
  border-color: var(--line);
  color: var(--ink);
}
body.admin input::placeholder,
body.admin textarea::placeholder { color: var(--ink-faint); }
body.admin ::-webkit-scrollbar-thumb { background: #3a302c; background-clip: padding-box; }

/* --- Login gate --- */
#loginScreen {
  position: fixed;
  inset: 0;
  z-index: 5000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  background: var(--paper-warm);
}
#loginScreen .card { width: 100%; max-width: 400px; text-align: center; }
#loginScreen input { margin-bottom: var(--s-3); }

/* --- Cards ---
   The admin markup uses .card where the rest of the site uses .content-card. */
.card {
  padding: var(--s-6);
  margin-bottom: var(--s-5);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.card h2 {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -.02em;
}
.card h2 i { color: var(--brand); font-size: .92rem; }
.card h3 {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
  font-size: .95rem;
  font-weight: 700;
}
.card label {
  display: block;
  margin-bottom: 6px;
  font-size: .8rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.card input,
.card select,
.card textarea {
  width: 100%;
  padding: 10px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: .88rem;
  font-family: inherit;
  margin-bottom: var(--s-3);
}
.card input:focus,
.card select:focus,
.card textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-tint);
}

/* --- Tab panes: same switching contract as before --- */
.tab-content { display: none; animation: page-in .3s var(--ease); }
.tab-content.active { display: block; }

/* --- Tables --- */
.table-responsive {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: var(--s-4);
}
.table-responsive table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: .85rem;
  white-space: nowrap;
}
.table-responsive th {
  position: sticky;
  top: 0;
  padding: 11px 14px;
  background: var(--paper-warm);
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.table-responsive td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}
.table-responsive tbody tr:last-child td { border-bottom: 0; }
.table-responsive tbody tr:hover { background: var(--line-soft); }
.table-responsive input,
.table-responsive select { margin-bottom: 0; }

/* --- Badges --- */
.badge, .order-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: var(--line-soft);
  border: 1px solid var(--line);
  font-size: .7rem;
  font-weight: 700;
  white-space: nowrap;
}
.badge-active { background: var(--ok-tint); color: var(--ok); border-color: transparent; }
.badge-locked { background: rgba(239, 68, 68, .14); color: #fca5a5; border-color: transparent; }
.badge-paid   { background: var(--ok-tint); color: var(--ok); border-color: transparent; }
.session-check { width: auto; margin: 0; accent-color: var(--brand); cursor: pointer; }

/* --- Coloured action buttons ---
   These are the destructive and constructive controls, so they stay
   explicitly coloured rather than inheriting the neutral button styles. */
.btn-maroon { background: var(--brand);  color: #fff; }
.btn-maroon:hover:not(:disabled) { background: var(--brand-deep); }
.btn-red    { background: #dc2626;       color: #fff; }
.btn-red:hover:not(:disabled) { background: #b91c1c; }
.btn-green  { background: #16a34a;       color: #fff; }
.btn-green:hover:not(:disabled) { background: #15803d; }
.btn-blue   { background: #2563eb;       color: #fff; }
.btn-blue:hover:not(:disabled) { background: #1d4ed8; }
.btn-brown  { background: #a16207;       color: #fff; }
.btn-brown:hover:not(:disabled) { background: #854d0e; }

/* --- Accordions --- */
.accordion-row { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: var(--s-3); overflow: hidden; }
.main-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  cursor: pointer;
  transition: background .2s var(--ease);
}
.main-row:hover { background: var(--line-soft); }
.accordion-content { display: none; padding: var(--s-5); border-top: 1px solid var(--line); background: var(--paper-warm); }
.accordion-row.open .accordion-content { display: block; }

/* --- Grouped bundle editor --- */
.group-card { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: var(--s-4); overflow: hidden; }
.group-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  background: var(--paper-warm);
  border-bottom: 1px solid var(--line);
  font-weight: 700;
  font-size: .9rem;
}
.group-bundle-select { padding: var(--s-4) var(--s-5); }
/* Each .check-group is one option: <div class="check-group"><input> Label</div>
   with the label as a bare text node, so the row itself has to be the
   control rather than a wrapper around styled <label> children. */
.check-group {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 9px 13px;
  margin-bottom: 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: .84rem;
  cursor: pointer;
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.check-group:hover { border-color: var(--brand); background: var(--brand-tint); }
.check-group input {
  width: auto;
  margin: 0;
  accent-color: var(--brand);
  cursor: pointer;
}
/* Checked rows read as selected without needing :has() support to be safe. */
.check-group:has(input:checked) {
  border-color: var(--brand);
  background: var(--brand-tint);
  font-weight: 600;
}

.bundle-mode-toggle {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  margin-bottom: var(--s-4);
  background: var(--paper-warm);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
}
.mode-toggle-btn {
  padding: 7px 16px;
  border-radius: var(--radius-pill);
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink-faint);
  transition: all .2s var(--ease);
}
.mode-toggle-btn.active { background: var(--brand); color: #fff; }

/* --- Weight validation --- */
.weight-label { font-size: .8rem; font-weight: 600; color: var(--ink-soft); }
.weight-input { max-width: 110px; }
.weight-status { font-size: .82rem; font-weight: 700; }
.weight-status.valid   { color: var(--ok); }
.weight-status.invalid { color: #f87171; }
.valid   { border-color: var(--ok) !important; }
.invalid { border-color: #f87171 !important; }
.visible { display: block !important; }

/* SweetAlert sits above the shell. */
.swal2-container { z-index: 9999 !important; }

/* --- Question editor ----------------------------------------------------
   These cards used to hardcode a light background and grey borders inline,
   from before the console went dark. The result was a pale card holding dark
   inputs, with grey-on-white labels that were close to unreadable. Styled
   from the tokens instead, so the editor follows whatever the theme is. */
.q-card {
  position: relative;
  background: var(--paper-warm);
  border: 1px solid var(--line);
  padding-right: var(--s-8);
}
.q-card > label {
  color: var(--ink);
  font-weight: 700;
}
.q-remove {
  position: absolute;
  top: var(--s-4); right: var(--s-4);
  width: 30px; height: 30px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-size: .75rem;
}
.q-text {
  width: 100%;
  min-height: 90px;
  margin-bottom: var(--s-3);
  resize: vertical;
}
/* Monospace run of column names in the import help. */
.fmt-header {
  display: inline-block;
  margin-top: 4px;
  padding: 3px 8px;
  border-radius: 5px;
  background: rgba(255, 255, 255, .08);
  font-family: ui-monospace, "Cascadia Code", Consolas, monospace;
  font-size: .8rem;
  line-height: 1.6;
}

/* In the dark console a note keeps its warning colour but on a dark ground -
   the light amber panel was legible only by accident. */
body.admin .ptc-note {
  background: rgba(251, 191, 36, .10);
  border-color: rgba(251, 191, 36, .32);
  color: #fcd34d;
}
body.admin .ptc-note strong,
body.admin .ptc-note em { color: #fde68a; }
body.admin .ptc-note i { color: var(--warn); }

/* Upload panels in the PTN and Modalitas tabs. */
.upload-panel {
  padding: var(--s-5);
  border-radius: var(--radius-sm);
  background: var(--paper-warm);
  border: 1px dashed var(--line);
}
.upload-panel h4 { font-size: .92rem; font-weight: 700; margin-bottom: var(--s-2); }
.upload-panel input[type="file"] { margin-bottom: var(--s-3); }

/* Backstop. Several inline styles in this console still set light values
   like color:#666 or color:#333 for labels and helper text, which vanish
   against a dark card. Anything that specific has to be overridden by
   something equally specific, hence the !important. */
body.admin [style*="color: #666"],
body.admin [style*="color:#666"],
body.admin [style*="color: #333"],
body.admin [style*="color:#333"],
body.admin [style*="color: #444"],
body.admin [style*="color:#444"] { color: var(--ink-soft) !important; }


/* ============================================================================
   21. SMALL UTILITIES
   ========================================================================== */

.stack   { display: flex; flex-direction: column; }
.row     { display: flex; align-items: center; }
.gap-2   { gap: var(--s-2); }
.gap-3   { gap: var(--s-3); }
.gap-4   { gap: var(--s-4); }
.center  { text-align: center; }
.mt-auto { margin-top: auto; }
.hidden  { display: none !important; }


/* ============================================================================
   22. LEADERBOARD
   Four boards share one table, so the board picker has to read as a switch
   between four separate things rather than as four filters on one list.
   ========================================================================== */

.board-switch { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.board-switch button {
    font: inherit;
    font-weight: 600;
    font-size: .85rem;
    padding: var(--s-2) var(--s-4);
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--paper);
    color: var(--ink-soft);
    cursor: pointer;
    transition: background .15s, color .15s, border-color .15s;
}
.board-switch button:hover { border-color: var(--brand); color: var(--brand); }
.board-switch button.active {
    background: var(--brand);
    border-color: var(--brand);
    color: #fff;
}

.board-row {
    display: grid;
    grid-template-columns: 2.5rem 1fr auto;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-3) 0;
    border-bottom: 1px solid var(--line);
}
.board-row:last-child { border-bottom: 0; }

/* The asking student's own row, wherever it appears. A board nobody can find
   themselves on is a list of strangers. */
.board-row.is-me {
    background: color-mix(in srgb, var(--brand) 8%, transparent);
    border-radius: var(--r-2, 10px);
    padding-inline: var(--s-3);
}

.board-rank {
    font-weight: 800;
    font-size: 1rem;
    text-align: center;
    color: var(--ink-soft);
}
/* A row is a button now: it opens the profile of whoever is standing on it.
   A real button rather than a clickable div, so it is reachable by keyboard
   and announces itself - and that means undoing what a browser gives a
   button by default, which would otherwise break the grid above. */
button.board-row {
    width: 100%;
    font: inherit;
    color: inherit;
    text-align: left;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--line);
    cursor: pointer;
    transition: background .15s ease;
}
button.board-row:hover {
    background: color-mix(in srgb, var(--brand) 6%, transparent);
}
button.board-row:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}

.board-row:nth-child(1) .board-rank { color: #b8860b; }
.board-row:nth-child(2) .board-rank { color: #7d8590; }
.board-row:nth-child(3) .board-rank { color: #a9642b; }

.board-name    { font-weight: 600; }
.board-total   { font-weight: 800; font-variant-numeric: tabular-nums; }
.board-sittings{ font-size: .72rem; color: var(--ink-soft); font-weight: 500; }

/* ── the Webinova-Q profile: level, badges, shelf ─────────────────────
   Deliberately plain for now. The shelf is three empty squares until the
   gacha ships, and the point of building it empty is that the tab does not
   get rebuilt around the toys when they arrive. */
.xp-bar {
    height: 10px;
    border-radius: 999px;
    background: var(--line, #e5e7eb);
    overflow: hidden;
}
.xp-bar > span {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, #6366f1, #8b5cf6);
    transition: width .4s ease;
}

.badge-list { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.badge-chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border: 1px solid var(--line, #e5e7eb);
    border-radius: 999px;
    font-size: .8rem;
    font-weight: 600;
}
.badge-chip i { color: #d97706; }

.shelf { display: flex; gap: var(--s-3); flex-wrap: wrap; }
.shelf-slot {
    flex: 1 1 6rem;
    min-width: 6rem;
    aspect-ratio: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px dashed var(--line, #e5e7eb);
    border-radius: var(--r-3, 14px);
    color: var(--ink-faint, #9ca3af);
    font-size: .78rem;
    text-align: center;
}
.shelf-slot.filled {
    border-style: solid;
    background: linear-gradient(140deg, #eef2ff, #e0e7ff);
    color: var(--ink);
    font-weight: 600;
    padding: 6px;
}
.shelf-slot img { width: 100%; height: 100%; object-fit: contain; }

/* Present for a screen reader, absent for everyone else. For the label on a
   field whose purpose is obvious from the one above it, where a visible
   second label would be clutter and no label at all is a field announced as
   nothing. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── somebody else's profile ──────────────────────────────────────────
   Opened from a board row. What a visitor sees is decided on the server;
   this only has to give it somewhere to sit. */
#peerBody {
    padding: var(--s-6);
    overflow-y: auto;
}
.peer-close {
    position: absolute;
    top: var(--s-4);
    right: var(--s-4);
    z-index: 2;
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 50%;
    background: var(--line-soft, #f1f0ef);
    color: var(--ink-soft);
    cursor: pointer;
    font-size: 1rem;
}
.peer-close:hover { background: var(--line); }
#peerModal .onboard-card { position: relative; }

.peer-title {
    display: inline-block;
    margin-top: 6px;
    padding: 4px 14px;
    border-radius: 999px;
    font-size: .74rem;
    font-weight: 700;
    color: #7c2d12;
    background: linear-gradient(135deg, #fde68a, #fcd34d);
}

/* ── the banner ───────────────────────────────────────────────────────
   The collection tab's own room. Everything else in the dashboard is a
   white card on a pale page, which is right for a result and wrong for
   this: a screen where you spend an exam's worth of gold on a chance
   should not look like a settings form. Dark, lit, and the characters
   themselves are the artwork - no extra pictures to commission or ship. */
.gc-banner {
    position: relative;
    overflow: hidden;
    border-radius: var(--r-4, 20px);
    padding: var(--s-6);
    margin-bottom: var(--s-6);
    color: #fff;
    background:
        radial-gradient(120% 140% at 80% 0%, rgba(168, 85, 247, .55), transparent 60%),
        radial-gradient(100% 120% at 0% 100%, rgba(56, 189, 248, .35), transparent 55%),
        linear-gradient(140deg, #1e1b4b, #312e81 55%, #4c1d95);
}
/* A slow drift of light. Cheap - one gradient, no images - and it is what
   stops the panel reading as a flat block of colour. */
.gc-banner::before {
    content: "";
    position: absolute;
    inset: -40%;
    background: conic-gradient(from 0deg, transparent, rgba(255, 255, 255, .10),
                               transparent 40%);
    animation: gc-sweep 14s linear infinite;
    pointer-events: none;
}
@keyframes gc-sweep { to { transform: rotate(1turn); } }

.gc-banner > * { position: relative; z-index: 1; }

.gc-banner-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--s-4);
    flex-wrap: wrap;
}
.gc-banner h2 {
    font-size: clamp(1.3rem, 3.6vw, 1.9rem);
    font-weight: 800;
    letter-spacing: -.02em;
    margin: 0;
}
.gc-banner .gc-sub {
    font-size: .84rem;
    opacity: .78;
    margin-top: 4px;
    max-width: 46ch;
    line-height: 1.55;
}

.gc-purse {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    background: rgba(255, 255, 255, .13);
    border: 1px solid rgba(255, 255, 255, .22);
    border-radius: 999px;
    padding: 9px 18px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.gc-purse i { color: #fcd34d; }

/* The banner's own artwork, and it is the student's own collection - not a
   preview of what they might win.

   It used to show the rarest characters whether or not they were owned, with
   the unowned ones greyed out. On the dark panel that read as three broken
   grey squares rather than as anything locked, and on a phone it pushed the
   buttons below the fold. So: owned only, and the row removes itself when
   there is nothing in it. A new student gets a clean banner; a student with
   a collection gets their own favourites on it. */
.gc-cast {
    display: flex;
    justify-content: center;
    align-items: flex-end;
    gap: var(--s-3);
    margin: var(--s-5) 0 var(--s-4);
}
.gc-cast:empty { display: none; }
.gc-cast img {
    width: clamp(3.4rem, 13vw, 6.5rem);
    aspect-ratio: 1;
    object-fit: contain;
    filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .45));
    animation: gc-float 4.5s ease-in-out infinite;
}
.gc-cast img:nth-child(2) { animation-delay: -1.5s; }
.gc-cast img:nth-child(3) { animation-delay: -3s; }
@keyframes gc-float {
    50% { transform: translateY(-9px); }
}
/* On a phone the banner has to earn its height. Its job is the purse and the
   two buttons; everything else is support, and support is what shrinks.
   Left alone it filled the whole screen and the collection below it was
   something you had to go looking for. */
@media (max-width: 560px) {
    .gc-cast { display: none; }
    .gc-banner { padding: var(--s-5) var(--s-4); }
    .gc-banner h2 { font-size: 1.32rem; }
    .gc-banner .gc-sub { font-size: .78rem; margin-top: 2px; }
    .gc-banner-top { gap: var(--s-3); }
    .gc-purse { padding: 7px 15px; font-size: .95rem; }
    .gc-actions { gap: var(--s-2); margin-top: var(--s-5); }
    /* Equal halves of one row. Two pills of different widths, one of them
       greyed out because the gold is short, read as a mistake rather than
       as a choice. */
    .gc-pull { flex: 1 1 0; padding: 13px 8px; font-size: .92rem; }
    .gc-odds-row { gap: 6px; margin-top: var(--s-3); }
    .gc-odds-chip { font-size: .6rem; padding: 4px 9px; letter-spacing: .02em; }
    .gc-hint { font-size: .72rem; margin-top: var(--s-3); line-height: 1.5; }
}

.gc-actions {
    display: flex;
    gap: var(--s-3);
    flex-wrap: wrap;
    justify-content: center;
}
.gc-pull {
    border: 0;
    border-radius: 999px;
    padding: 13px 26px;
    font: inherit;
    font-weight: 800;
    cursor: pointer;
    transition: transform .15s ease, filter .15s ease;
    white-space: nowrap;
}
.gc-pull:hover:not(:disabled) { transform: translateY(-2px); filter: brightness(1.07); }
.gc-pull:disabled { opacity: .45; cursor: not-allowed; }
.gc-pull.one {
    background: rgba(255, 255, 255, .16);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, .3);
}
.gc-pull.ten {
    background: linear-gradient(135deg, #fbbf24, #f59e0b);
    color: #3b2500;
    box-shadow: 0 8px 22px rgba(251, 191, 36, .34);
}
.gc-hint {
    text-align: center;
    font-size: .76rem;
    opacity: .72;
    margin-top: var(--s-4);
    line-height: 1.6;
}

.gc-odds-row {
    display: flex;
    justify-content: center;
    gap: var(--s-2);
    flex-wrap: wrap;
    margin-top: var(--s-4);
}
.gc-odds-chip {
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    border-radius: 999px;
    padding: 5px 13px;
    background: rgba(255, 255, 255, .12);
    border: 1px solid rgba(255, 255, 255, .2);
}
.gc-odds-chip.t-secret { background: linear-gradient(135deg, rgba(251,191,36,.34), rgba(236,72,153,.34)); }
.gc-odds-chip.t-rare   { background: rgba(168, 85, 247, .34); }

@media (prefers-reduced-motion: reduce) {
    .gc-banner::before, .gc-cast img { animation: none; }
}

/* ── the collection ───────────────────────────────────────────────────
   Three frames, and the difference between them is the whole reward. Grey
   is flat on purpose: if a common looked lively there would be nothing for
   a rare to be. The aurora is reserved for ten stars and is the only thing
   here that cannot be rushed - it takes about eleven copies of one
   character, which is months. */
.gacha-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
    gap: var(--s-4);
}

.gacha-card {
    position: relative;
    border-radius: var(--r-3, 14px);
    padding: 3px;
    background: #d1d5db;
    transition: transform .18s ease;
}
.gacha-card:hover { transform: translateY(-3px); }

.gacha-card .gc-inner {
    background: var(--surface, #fff);
    border-radius: calc(var(--r-3, 14px) - 3px);
    padding: var(--s-3);
    text-align: center;
    height: 100%;
}
.gacha-card img {
    width: 100%;
    aspect-ratio: 1;
    object-fit: contain;
    display: block;
}
.gc-name { font-size: .82rem; font-weight: 700; margin-top: 4px; }
.gc-tier { font-size: .66rem; letter-spacing: .04em; text-transform: uppercase;
           color: var(--ink-soft); font-weight: 700; }
.gc-odds { font-size: .64rem; color: var(--ink-faint, #9ca3af); }

/* Not owned: the card still shows, because a collection screen that hides
   what you are missing cannot tell you what to chase. */
.gacha-card.locked .gc-inner { opacity: .45; }
.gacha-card.locked img { filter: grayscale(1) contrast(.6); }

.gacha-card.tier-rare   { background: linear-gradient(140deg, #a855f7, #6d28d9, #c084fc); }
.gacha-card.tier-secret { background: linear-gradient(140deg, #f59e0b, #ec4899, #8b5cf6); }

.gacha-card.ethereal { padding: 4px; background: transparent; }
.gacha-card.ethereal::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: conic-gradient(from 0deg, #22d3ee, #a78bfa, #f472b6,
                               #fbbf24, #34d399, #22d3ee);
    animation: gc-spin 6s linear infinite;
}
.gacha-card.ethereal .gc-inner { position: relative; z-index: 1; }
@keyframes gc-spin { to { transform: rotate(1turn); } }

.gc-stars {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 1px;
    margin-top: 3px;
    min-height: 12px;
    font-size: .58rem;
    color: #f59e0b;
}
.gc-stars.full { color: #8b5cf6; }

/* ── the reveal ───────────────────────────────────────────────────────
   A courier brings a parcel, the parcel opens. The parcel's glow is the
   colour of the best thing inside it, which turns the two seconds before it
   opens into the part worth watching. */
.gacha-stage {
    position: fixed;
    inset: 0;
    z-index: 900;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(12, 14, 28, .88);
    backdrop-filter: blur(3px);
    overflow: hidden;
}
.gacha-stage.on { display: flex; }

.gc-skip {
    position: absolute;
    top: var(--s-4);
    right: var(--s-4);
    z-index: 5;
    background: rgba(255, 255, 255, .14);
    color: #fff;
    border: 0;
    border-radius: 999px;
    padding: 8px 18px;
    font: inherit;
    font-size: .82rem;
    font-weight: 600;
    cursor: pointer;
}
.gc-skip:hover { background: rgba(255, 255, 255, .26); }

/* The courier rides in and **stops**, and nothing else happens until the
   student taps him. Waiting through a fixed animation is a loading screen;
   tapping the parcel is the moment the pull belongs to them. */
.gc-rider {
    position: absolute;
    bottom: 18%;
    left: -45%;
    width: min(52vw, 20rem);
    translate: -50% 0;
    cursor: pointer;
    background: none;
    border: 0;
    padding: 0;
}
.gc-rider.arrive { animation: gc-ride-in 1.5s cubic-bezier(.2, .75, .3, 1) forwards; }
.gc-rider.leave  { animation: gc-ride-out 1.1s cubic-bezier(.5, 0, .85, .4) forwards; }
@keyframes gc-ride-in  { to { left: 50%; } }
@keyframes gc-ride-out { from { left: 50%; } to { left: 150%; } }

/* A gentle bob while he waits, so a stopped courier reads as waiting for you
   rather than as an animation that has frozen. */
.gc-rider.waiting img { animation: gc-idle 1.6s ease-in-out infinite; }
@keyframes gc-idle { 50% { transform: translateY(-7px); } }
.gc-rider img { width: 100%; display: block; }

.gc-tap {
    position: absolute;
    bottom: 9%;
    left: 50%;
    translate: -50% 0;
    text-align: center;
    color: #fff;
    font-weight: 700;
    font-size: .92rem;
    opacity: 0;
    pointer-events: none;
    transition: opacity .4s ease;
}
.gc-tap.on { opacity: 1; animation: gc-breathe 1.7s ease-in-out infinite; }
.gc-tap i { display: block; font-size: 1.4rem; margin-bottom: 6px; }
@keyframes gc-breathe { 50% { opacity: .5; } }

.gc-box {
    position: relative;
    width: min(52vw, 15rem);
    opacity: 0;
}
.gc-box.drop { animation: gc-drop .5s cubic-bezier(.3, 1.5, .5, 1) forwards; }
.gc-box.shake { animation: gc-shake .42s ease-in-out 3; }
@keyframes gc-drop {
    0%   { opacity: 0; transform: translateY(-160px) scale(.7); }
    100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes gc-shake {
    0%, 100% { transform: rotate(0) scale(1); }
    25%      { transform: rotate(-7deg) scale(1.04); }
    75%      { transform: rotate(7deg) scale(1.04); }
}
.gc-box img { width: 100%; display: block; }

/* The tell. Tinted by the best tier in the batch, before anything opens. */
.gc-box::after {
    content: "";
    position: absolute;
    inset: -45%;
    border-radius: 50%;
    z-index: -1;
    opacity: 0;
    transition: opacity .6s ease;
}
.gc-box.glow-common::after { background: radial-gradient(circle, rgba(203,213,225,.45), transparent 66%); opacity: 1; }
.gc-box.glow-rare::after   { background: radial-gradient(circle, rgba(168,85,247,.75), transparent 68%); opacity: 1; }
.gc-box.glow-secret::after { background: radial-gradient(circle, rgba(251,191,36,.85), transparent 70%); opacity: 1;
                             animation: gc-pulse 1s ease-in-out infinite; }
@keyframes gc-pulse { 50% { transform: scale(1.12); } }

.gc-burst {
    position: absolute;
    /* Centred with `translate` rather than inside `transform`, because the
       flash animation below owns `transform` - putting the centring there
       too meant the first keyframe threw it away, and the burst rendered
       wherever its static position happened to fall. That is why it sat off
       to one side of the card on a phone. */
    top: 50%;
    left: 50%;
    translate: -50% -50%;
    width: min(92vw, 34rem);
    opacity: 0;
    pointer-events: none;
}
.gc-burst.on { animation: gc-flash 1.1s ease-out forwards; }
@keyframes gc-flash {
    0%   { opacity: 0; transform: scale(.35) rotate(0); }
    35%  { opacity: 1; }
    100% { opacity: .55; transform: scale(1.25) rotate(22deg); }
}

.gc-haul {
    position: relative;
    z-index: 2;
    display: none;
    width: min(94vw, 46rem);
    max-height: 84vh;
    overflow-y: auto;
    padding: var(--s-5);
}
.gc-haul.on { display: block; animation: gc-in .4s ease both; }
@keyframes gc-in { from { opacity: 0; transform: translateY(14px); } }

/* auto-FIT with a capped track and centred content, not auto-fill with 1fr.
   auto-fill keeps making empty columns, so a single card was laid in column
   one and sat against the left edge instead of under the burst. */
.gc-haul-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(6.4rem, 8rem));
    justify-content: center;
    gap: var(--s-3);
}
.gc-haul .gacha-card { animation: gc-pop .34s cubic-bezier(.3, 1.6, .5, 1) both; }
@keyframes gc-pop { from { opacity: 0; transform: scale(.55); } }

.gc-badge {
    position: absolute;
    top: -7px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    background: #ef4444;
    color: #fff;
    font-size: .58rem;
    font-weight: 800;
    letter-spacing: .04em;
    padding: 2px 8px;
    border-radius: 999px;
    white-space: nowrap;
}
.gc-badge.dupe { background: #64748b; }

.gc-done {
    display: block;
    margin: var(--s-5) auto 0;
    background: #fff;
    color: #111827;
    border: 0;
    border-radius: 999px;
    padding: 11px 34px;
    font: inherit;
    font-weight: 700;
    cursor: pointer;
}

/* Everything above is decoration. Somebody who has asked their machine to
   stop moving things gets the result without the theatre - the pull still
   happens, it simply arrives. */
@media (prefers-reduced-motion: reduce) {
    .gc-rider, .gc-box, .gc-burst, .gc-tap { display: none !important; }
    .gacha-card.ethereal::before { animation: none; }
    .gc-haul .gacha-card, .gc-haul.on { animation: none; }
}


/* ═══════════════════════════════════════════════════════════════════════
   THE WEBINOVA-Q THEME
   ═══════════════════════════════════════════════════════════════════════

   Dark, violet, and lit. Applied by `data-theme="q"` on <html>, which at
   present is the Webinova-Q dashboard and nothing else.

   **Why it is scoped rather than global.** The two halves of this product
   answer to different people. The portal is where a lecturer, a school or a
   parent looks; the maroon there is deliberate and the top of this file
   explains why - it reads as academic and institutional. Webinova-Q is where
   a fifteen-year-old collects plush toys, watches a courier deliver a parcel
   and checks whether they are still 248th. Those want different rooms.

   **Why it is only tokens.** Every component in this file was already built
   on the variables below, so redefining them turns the whole dashboard over
   at once: the cards, the tables, the chips, the forms, the leaderboard rows
   and the collection all follow without being rewritten. What is left after
   the tokens is the handful of rules that named a colour outright, and those
   are corrected underneath rather than everywhere.

   A note on contrast: the text colours here are lifted well clear of the
   surfaces on purpose. Dark themes fail by being stylish at 3:1, and this
   one is read on a phone, in daylight, by somebody revising. */

:root[data-theme="q"] {
    /* Violet rather than maroon. Maroon is the institution's colour and it
       does not belong on the screen where the gacha lives. */
    --brand:        #7c5cff;
    --brand-deep:   #5b3fd9;
    --brand-bright: #a78bfa;
    --brand-tint:   #1d1740;
    --gold:         #fbbf24;

    /* Ink, inverted. Not pure white: a faint violet in the text keeps it
       sitting in the same room as the background instead of glaring off it. */
    --ink:       #f4f2ff;
    --ink-soft:  #aaa3cd;
    --ink-faint: #746c9b;

    /* Paper becomes surface. `--paper` is the card, `--paper-warm` the page
       behind it - the same relationship as the light theme, upside down. */
    --paper:      #171331;
    --paper-warm: #0d0a1e;
    --surface:    #171331;
    --line:       #2b2450;
    --line-soft:  #211b42;

    --ok:      #34d399;
    --ok-tint: #10291f;
    --warn:    #fbbf24;
    --info:    #38bdf8;

    /* Shadows do almost nothing on a dark ground - there is no light for
       them to block. Depth here comes from the surface being lighter than
       the page, plus a rim of violet on the things that matter. */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
    --shadow:    0 6px 18px rgba(0, 0, 0, .45);
    --shadow-lg: 0 24px 60px rgba(0, 0, 0, .6);
}

:root[data-theme="q"] body {
    background: var(--paper-warm);
    color: var(--ink);
}

/* The page behind the cards, with two soft lights in it so a long scroll is
   not a flat black field. Fixed, so it does not travel with the content. */
:root[data-theme="q"] body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(60% 45% at 12% 0%, rgba(124, 92, 255, .20), transparent 70%),
        radial-gradient(50% 40% at 100% 30%, rgba(56, 189, 248, .10), transparent 70%);
}

/* ── the rules that named a colour outright ───────────────────────────
   Everything here was written against a white page. None of it is a
   redesign; it is the same rule, told where it is. */

:root[data-theme="q"] .sidebar,
:root[data-theme="q"] .top-bar {
    background: rgba(13, 10, 30, .92);
    border-color: var(--line);
}
@media (max-width: 992px) {
    /* Still no backdrop-filter on a phone - see the note on the sticky
       header further up. The reason does not change with the colour. */
    :root[data-theme="q"] .top-bar { background: rgba(13, 10, 30, .98); }
}

:root[data-theme="q"] .soft-card,
:root[data-theme="q"] .content-card,
:root[data-theme="q"] .card,
:root[data-theme="q"] .onboard-card {
    background: var(--paper);
    border: 1px solid var(--line);
}

/* The stat tiles were pale washes of colour on white. On a dark ground the
   same idea is a dark wash with a lit rim and a brighter icon. */
:root[data-theme="q"] .soft-stat {
    background: linear-gradient(160deg, rgba(124, 92, 255, .14), rgba(124, 92, 255, .04));
    border: 1px solid var(--line);
}
:root[data-theme="q"] .tint-indigo { background: linear-gradient(160deg, #241c54, #191340); }
:root[data-theme="q"] .tint-sky    { background: linear-gradient(160deg, #10304a, #0e2036); }
:root[data-theme="q"] .tint-mint   { background: linear-gradient(160deg, #123a2e, #0e2a22); }
:root[data-theme="q"] .tint-peach  { background: linear-gradient(160deg, #40290f, #2c1d0c); }
:root[data-theme="q"] .tint-rose   { background: linear-gradient(160deg, #3d1428, #2a0e1c); }
:root[data-theme="q"] .tint-sky i  { color: #7dd3fc; }
:root[data-theme="q"] .tint-mint i { color: #6ee7b7; }
:root[data-theme="q"] .tint-peach i{ color: #fcd34d; }
:root[data-theme="q"] .tint-rose i { color: #fda4af; }
:root[data-theme="q"] .tint-indigo i { color: #c4b5fd; }

:root[data-theme="q"] .form-input,
:root[data-theme="q"] input,
:root[data-theme="q"] select,
:root[data-theme="q"] textarea {
    background: #120e2a;
    border-color: var(--line);
    color: var(--ink);
}
:root[data-theme="q"] .form-input:focus,
:root[data-theme="q"] select:focus {
    border-color: var(--brand-bright);
    box-shadow: 0 0 0 3px rgba(124, 92, 255, .22);
}
:root[data-theme="q"] option { background: #120e2a; color: var(--ink); }

:root[data-theme="q"] .btn-outline {
    border-color: var(--line);
    color: var(--ink);
    background: transparent;
}
:root[data-theme="q"] .btn-outline:hover {
    border-color: var(--brand-bright);
    background: rgba(124, 92, 255, .12);
}
:root[data-theme="q"] .btn-primary {
    background: linear-gradient(135deg, #7c5cff, #a855f7);
    border: 0;
    color: #fff;
}

:root[data-theme="q"] .empty-state { background: var(--line-soft); }
:root[data-theme="q"] table th { color: var(--ink-soft); }
:root[data-theme="q"] table td { border-color: var(--line); }

/* The collection, which was designed light and needs the least changing -
   the frames were always meant to glow. */
:root[data-theme="q"] .gacha-card { background: var(--line); }
:root[data-theme="q"] .gacha-card .gc-inner { background: #1b1638; }
:root[data-theme="q"] .gacha-card.locked .gc-inner { opacity: .38; }
:root[data-theme="q"] .shelf-slot { border-color: var(--line); }
:root[data-theme="q"] .shelf-slot.filled {
    background: linear-gradient(140deg, #251d57, #1a1440);
}
:root[data-theme="q"] .badge-chip { border-color: var(--line); background: #1b1638; }
:root[data-theme="q"] .xp-bar { background: var(--line); }
:root[data-theme="q"] .peer-close { background: var(--line); color: var(--ink); }
:root[data-theme="q"] .board-switch button {
    background: transparent;
    border-color: var(--line);
    color: var(--ink-soft);
}
:root[data-theme="q"] .board-switch button.active {
    background: var(--brand);
    border-color: var(--brand);
    color: #fff;
}
:root[data-theme="q"] .gc-done { background: #fff; color: #171331; }

/* `body.tobk` sets its own indigo on the body element, which sits below
   <html> and therefore wins for everything inside it. Restated here, or the
   theme's violet would be overridden by the page it is theming. */
:root[data-theme="q"] body.tobk {
    --brand:        #7c5cff;
    --brand-deep:   #5b3fd9;
    --brand-bright: #a78bfa;
    --brand-tint:   #1d1740;
}
:root[data-theme="q"] body.tobk .sidebar {
    background: rgba(13, 10, 30, .94);
    border-right: 1px solid var(--line);
}
:root[data-theme="q"] body.tobk .search-box input {
    background: #120e2a;
    border-color: var(--line);
}

/* The ticket shop and the booking calendar were the two screens built most
   firmly against white. */
:root[data-theme="q"] .ticket-pack {
    background: #1b1638;
    border-color: var(--line);
}
:root[data-theme="q"] .ticket-pack.featured {
    background: linear-gradient(160deg, #271f5c, #1b1638 62%);
    border-color: rgba(124, 92, 255, .5);
    box-shadow: 0 0 0 1px rgba(124, 92, 255, .25);
}
:root[data-theme="q"] .btn-outline-brand {
    background: transparent;
    color: var(--brand-bright);
    border-color: var(--brand);
}
:root[data-theme="q"] .cal-day { background: #1b1638; border-color: var(--line); }
:root[data-theme="q"] .cal-day.is-future { background: rgba(255, 255, 255, .04); }
:root[data-theme="q"] .pill-closed { background: #241e46; color: var(--ink-faint); }

/* ── the level strip ──────────────────────────────────────────────────
   Directly under the header, on every tab. What turns a dashboard into a
   game is being told where you are before you ask, and this is the cheapest
   possible version of that: it is filled from the profile the Profil tab
   fetches anyway, so it costs nothing of its own. */
.q-strip {
    display: flex;
    align-items: center;
    gap: var(--s-5);
    flex-wrap: wrap;
    padding: var(--s-4) var(--s-6);
    border-bottom: 1px solid var(--line);
    background: linear-gradient(100deg, rgba(124, 92, 255, .16), rgba(56, 189, 248, .06));
}
.q-strip[hidden] { display: none; }

.q-lv {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-width: 12rem;
    flex: 1 1 14rem;
}
.q-lv-badge {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: 12px;
    font-weight: 800;
    font-size: .95rem;
    color: #fff;
    background: linear-gradient(140deg, #7c5cff, #a855f7);
    box-shadow: 0 6px 16px rgba(124, 92, 255, .4);
}
.q-lv-bar {
    flex: 1;
    min-width: 0;
}
.q-lv-bar .xp-bar { height: 7px; margin-top: 5px; }
.q-lv-label {
    display: flex;
    justify-content: space-between;
    gap: var(--s-3);
    font-size: .72rem;
    color: var(--ink-soft);
    font-weight: 600;
}

.q-chips { display: flex; gap: var(--s-2); flex-wrap: wrap; }
.q-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 7px 13px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: rgba(255, 255, 255, .05);
    font-size: .8rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.q-chip i { font-size: .82rem; }
.q-chip.gold i { color: #fbbf24; }
.q-chip.rank i { color: #7dd3fc; }
.q-chip.toys i { color: #f472b6; }

@media (max-width: 640px) {
    .q-strip { padding: var(--s-3) var(--s-4); gap: var(--s-3); }
    .q-chip { font-size: .74rem; padding: 6px 11px; }
    .q-lv { flex-basis: 100%; }
}

/* The one thing that must not go dark. A certificate and a result sheet are
   printed and handed to somebody; they stay on paper. */
@media print {
    :root[data-theme="q"] { --paper: #fff; --paper-warm: #fff; --ink: #000; }
    :root[data-theme="q"] body::before { display: none; }
}
