/* ==========================================================================
     1. ROOT VARIABLES & THEME — colors, fonts, spacing tokens; also the
     light-theme override block (same variable names, different values)
     ========================================================================== */

  :root{
    --navy-900:#0b1330;
    --navy-800:#101a3d;
    --navy-700:#16204d;
    /* Dedicated background roles — kept separate from navy-900/800/700
       above because those three are ALSO reused as fixed "dark ink" colors
       (SVG icon details, text sitting on a bright badge) that must stay
       dark in both themes. Only these three actually repaint for light mode. */
    /* Redesigned default palette (Aug 2026) — deeper, less saturated navy
       so panels read as raised surfaces rather than bordered boxes, and an
       indigo accent with enough contrast to sit under white/light ink at
       small sizes, which the old sky-blue #5db8ff didn't have room for. */
    --page-bg:#0a0e17;
    --surface-bg:#0d1220;
    --header-bg:#131a2c;
    --panel:#141a2a;
    --line:#232b42;
    --text:#f2f4fa;
    --text-dim:#8b93aa;
    --accent:#6f8cff;
    /* Ink for text sitting ON an accent-coloured button. Dark themes
       have a bright accent, so the default is the darkest navy — but a
       theme whose accent is itself dark has to say so, or its buttons
       read dark-on-dark (Daylight and Cranberry did: 2.2:1 and 2.1:1,
       both far under the 4.5:1 that ordinary text needs). #6f8cff only
       clears 4.5:1 against a dark ink, not white (≈3.1:1) — same rule,
       different side of it than the old sky-blue accent needed. */
    --on-accent:#0a0e17;

    /* Official AAUP requirement-category palette, applied to every plan:
       green University Req., gold University Elec., blue College Req.,
       amber Specialization Req., pink Specialization Elec., red Free Elec.
       (--eng is folded into the University-Req green since English courses
       are university requirements — the "English" category is retired.) */
    --core:#b06e2a;      --core-tx:#ffeed8;
    --skills:#2f8f57;    --skills-tx:#e7f7ee;
    --math:#2f6bb0;      --math-tx:#e2effb;
    --dept:#bd5f96;      --dept-tx:#fbe6f2;
    --eng:#2f8f57;       --eng-tx:#e7f7ee;
    --uni:#a8901f;       --uni-tx:#f9f2cf;
    --free:#b03a38;      --free-tx:#fbdedd;
    --misc:#1f7a8c;      --misc-tx:#d8f3f7;
    --intern:#2e8fae;    --intern-tx:#dbf1f8;
    --summer:#7a2b46;    --summer-tx:#f8dce8;

    --prereq:#e0538c;
    --connect:#e0a83f;

    /* Notice, not error. Used by the heavy-semester note (js/28-imported.js).
       Kept theme-neutral on purpose: this amber sits legibly on all six
       grounds, light and dark, so it is defined once rather than per theme. */
    --warn:#c9821c;
  }

  *{box-sizing:border-box;}
  /* ================= SEARCH — shared styles ================= */

  :root{
    --unlock:#2fbf71; --unlock-tx:#e7fbef;
    --search-bg:#101627;
  }
  /* Deliberately muted and a little "paper" rather than stark white — a
     university-facing tool reads as more formal with soft blue-gray
     surfaces and deep navy ink than with bright white and saturated blue.
     Note --navy-900/800/700 are NOT redefined here: those three also serve
     as fixed dark "ink" (SVG icon details, text sitting on a bright badge)
     and must stay dark in both themes — see --page-bg/--surface-bg/
     --header-bg above, which is what actually repaints for light mode. */

  html[data-theme="light"]{
    --page-bg:#e7ebf1;
    --surface-bg:#f9fafb;
    --header-bg:#eef1f6;
    --panel:#f5f6f9;
    --line:#ccd3e0;
    --text:#1b2a3f;
    --text-dim:#55647a;
    --accent:#1b4f8c;
    --search-bg:#eef1f6;
    --on-accent:#ffffff;
  }

  html[data-theme="light"] .watermark-layer{color:rgba(27,42,63,.22);}

  /* Four more themes beyond the original dark/light pair — same nine
     variables every time, so every component that already themes itself
     correctly (by reading var(--text)/var(--accent)/etc rather than a
     hardcoded color) picks these up with no further changes anywhere else
     in this file. Two lean light, two lean dark, so there's a genuine
     option in both families rather than only reskinning one. */

  html[data-theme="cranberry"]{
    --page-bg:#f3e9dd;
    --surface-bg:#faf3ea;
    --header-bg:#efe0cd;
    --panel:#f7eee1;
    --line:#d9c4a8;
    --text:#3a1f1a;
    --text-dim:#7a5c4a;
    --accent:#8c1f3d;
    --search-bg:#f7ece0;
    --on-accent:#ffffff;
  }
  html[data-theme="cranberry"] .watermark-layer{color:rgba(58,31,26,.22);}

  html[data-theme="botanical"]{
    --page-bg:#eef1e6;
    --surface-bg:#f7f9f2;
    --header-bg:#e6ead9;
    --panel:#f2f5ec;
    --line:#c9d2b8;
    --text:#263424;
    --text-dim:#5c6b52;
    --accent:#b3702f;
    --search-bg:#f2f5ec;
  }
  html[data-theme="botanical"] .watermark-layer{color:rgba(38,52,36,.22);}

  html[data-theme="indigo"]{
    --page-bg:#150f2e;
    --surface-bg:#1c1440;
    --header-bg:#241a52;
    --panel:#201753;
    --line:#3c2e72;
    --text:#ede9fb;
    --text-dim:#a99fd6;
    --accent:#a56bff;
    --search-bg:#1a1240;
  }

  html[data-theme="amber"]{
    --page-bg:#1c1410;
    --surface-bg:#241a14;
    --header-bg:#2c1f17;
    --panel:#261c15;
    --line:#4a3527;
    --text:#f5ece0;
    --text-dim:#c2a688;
    --accent:#ff9f43;
    --search-bg:#201712;
  }

  /* MY COLOURS (js/61-theme-custom.js) — the one theme whose values come
     from the student rather than from this file. The editor writes nine
     --c-* custom properties onto <html> and this block maps them onto the
     same nine names every other theme sets, so nothing downstream needs to
     know a custom theme exists. The fallbacks are the dark theme's values,
     which is what you see if the stored colours are ever missing. */
  html[data-theme="custom"]{
    --page-bg:var(--c-page,#0a0e17);
    --surface-bg:var(--c-surface,#0d1220);
    --header-bg:var(--c-header,#131a2c);
    --panel:var(--c-panel,#141a2a);
    --line:var(--c-line,#232b42);
    --text:var(--c-text,#f2f4fa);
    --text-dim:var(--c-dim,#8b93aa);
    --accent:var(--c-accent,#6f8cff);
    --search-bg:var(--c-search,#101627);
    --on-accent:var(--c-on-accent,#0a0e17);
  }
  html[data-theme="custom"] .watermark-layer{color:var(--c-watermark,rgba(255,255,255,.05));}

  /* ==========================================================================
     2. GLOBAL RESET & BASE — box-sizing, body background/font, outer .sheet frame
     ========================================================================== */

  body{
    margin:0;
    background:var(--page-bg);
    font-family:'Cairo', -apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    color:var(--text);
    padding:20px;
  }

  /* Self-hosted, not linked from Google Fonts — this app precaches its
     whole shell for offline use (see sw.js's own header comment) and has
     no other CDN dependency anywhere (even the QR encoder is homegrown for
     the same reason), so a live font request would be the one thing that
     silently needed a network connection. Cairo is a variable font (one
     file per subset covers the whole 200-1000 weight range), and one
     family serves both scripts this app actually needs: unlike the
     previous Bricolage Grotesque / Instrument Sans pairing, which was
     Latin-only and phone-scoped for exactly that reason, Cairo has native
     Arabic glyphs, so it can be the one typeface for every page, in every
     language, on every platform, without any RTL text silently falling
     back to a mismatched system font. Split into two @font-face rules by
     unicode-range (Arabic / Latin) rather than one merged file: a browser
     only fetches the subset a page's actual characters need, so an
     English-only session never pays for the Arabic glyphs and vice versa.
     Registered in sw.js's CORE list alongside everything else that makes
     the app open offline. */
  @font-face{
    font-family:'Cairo'; font-style:normal; font-weight:200 1000;
    font-display:swap; src:url('../assets/fonts/cairo-arabic.woff2') format('woff2');
    unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC, U+200C-200E;
  }
  @font-face{
    font-family:'Cairo'; font-style:normal; font-weight:200 1000;
    font-display:swap; src:url('../assets/fonts/cairo-latin.woff2') format('woff2');
    unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
  }
  h1, h2, h3{font-family:'Cairo', -apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; font-weight:800;}

  /* Every scroll container in the app inherits this — the page itself,
     every modal body, every overflow:auto list — so nothing falls back to
     the browser's plain default scrollbar (light gray on every theme,
     including the dark ones, where it stands out like an error). A few
     spots (.assessment-rows, .line-list) already had their own copy of the
     same rule for a slimmer 5px thumb; those still win by specificity. */
  /* ---------- tapping something should not repaint it ----------
     Chrome and Brave on Android paint a translucent blue-grey over the
     WHOLE box of whatever was tapped, for a moment after the tap. On a
     small button nobody notices. On the things this app makes tappable —
     a My Path category card, a year bar, a course card — the tapped box is
     most of the screen, so the card and every row inside it went flat blue
     and looked broken, which is what a student reported.

     It is turned off everywhere and replaced with real feedback: each of
     those controls already has its own :hover, :active and :focus-visible
     styling, which says the same thing without covering anything up. */
  button, a, summary, [role="button"], [role="checkbox"], [data-ro-expand],
  .course, .plan-card, .dash-quicklink, .sb-item, .sb-mrow, .sb-tab, .lib-row{
    -webkit-tap-highlight-color:transparent;
  }
  /* And a long press is a gesture in this app — hold a course to trace it,
     hold a year to see what is open — so a control's own label must not be
     selectable, or holding one starts a text selection over it instead.
     Deliberately scoped to controls: real content (a message on the wall, an
     email in Contacts, a course name in a popup) stays selectable. */
  button, [role="button"], .sb-tab, .imp-year-toggle, .course, .ro-node-wrap,
  .eng-opt, .lib-shelf-head{
    -webkit-user-select:none; user-select:none;
  }
  /* When a selection IS made, it should look like this app rather than like
     the browser's default blue on top of a dark theme. */
  ::selection{ background:var(--accent); color:var(--on-accent); }

  html{ scrollbar-width:thin; scrollbar-color:var(--line) var(--surface-bg); }
  ::-webkit-scrollbar{ width:10px; height:10px; }
  ::-webkit-scrollbar-track{ background:var(--surface-bg); }
  ::-webkit-scrollbar-thumb{ background:var(--line); border-radius:999px; border:2px solid var(--surface-bg); }
  ::-webkit-scrollbar-thumb:hover{ background:var(--accent); }

  .sheet{
    max-width:1500px;
    margin:0 auto;
    border:1px solid var(--line);
    border-radius:10px;
    /* clip, not hidden. Both clip a child to the rounded corners, but
       "hidden" also makes this a scroll container — and a position:sticky
       descendant sticks within its nearest scroll container, which here is
       a box that never scrolls. That is why the progress meter would not
       travel. "clip" does the clipping without the scroll port.
       hidden stays first as the fallback for a browser without clip: the
       corners are still right, the meter just does not stick. */
    overflow:hidden;
    overflow:clip;
    background:var(--surface-bg);
  }

  /* ==========================================================================
     3. HEADER, BRAND & TOP NAVIGATION — logo/brand block, home/lang/theme/dev
     buttons, footer (header and footer share the same "page chrome" styling)
     ========================================================================== */
  /* ---------- header ---------- */

  header{
    background:var(--header-bg);
    border-bottom:2px solid var(--accent);
    padding:18px 32px;
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:24px;
  }

  .brand{display:flex;align-items:center;gap:14px;}

  .brand .mark{
    width:46px;height:46px;border-radius:10px;
    background:linear-gradient(135deg, color-mix(in srgb, var(--accent) 22%, var(--surface-bg)), var(--surface-bg));
    border:1px solid var(--accent);
    display:flex;align-items:center;justify-content:center;
    overflow:hidden;
  }

  .mark-icon{width:30px;height:30px;object-fit:contain;}

  .brand h1{margin:0;font-size:15px;font-weight:700;letter-spacing:.03em;}

  .brand p{margin:2px 0 0;font-size:11.5px;color:var(--text-dim);}

  .title-block{text-align:center;}

  .icon-row{display:flex;align-items:center;justify-content:center;gap:16px;}

  .theme-icon{width:40px;height:40px;object-fit:contain;border-radius:10px;flex:0 0 auto;}

  .title-block .en{font-size:22px;font-weight:700;line-height:1.25;}

  .title-block .en em{color:var(--accent);font-style:normal;display:block;}

  .ar-block{text-align:right;direction:rtl;display:none;}

  .ar-block .ar1{font-size:16px;font-weight:700;}

  .ar-block .ar2{font-size:11.5px;color:var(--text-dim);margin-top:2px;}

  /* Only the active language's title shows at a time — pressing the
     language toggle switches between them, rather than showing both
     permanently side by side. */
  .rtl-mode .title-block{display:none;}
  .rtl-mode .ar-block{display:block;}

  /* flex-shrink stays available: with 0 0 auto this row kept its full content
     width (~604px) on a 360px phone, so Course Library, Export Plan and
     Contribute sat off the right edge with nothing able to scroll to them.
     Allowing it to shrink is what lets the flex-wrap below actually wrap. */
  .header-actions{display:flex;align-items:center;gap:10px;flex:0 1 auto;min-width:0;}

  .lang-toggle{
    display:flex;align-items:center;gap:6px;
    padding:7px 14px;border-radius:999px;
    border:1px solid var(--accent);
    background:color-mix(in srgb, var(--accent) 10%, transparent);
    color:var(--accent);
    font-size:12.5px;font-weight:700;
    cursor:pointer;white-space:nowrap;
    transition:background .15s ease;
    font-family:inherit;
  }

  .lang-toggle:hover{background:color-mix(in srgb, var(--accent) 22%, transparent);}

  header{flex-wrap:wrap;row-gap:10px;}

  .plan-page.rtl-mode footer{flex-direction:row-reverse;}

  .plan-page.rtl-mode footer span{flex-direction:row-reverse;}

  .plan-page.rtl-mode .brand{flex-direction:row-reverse;}

  .plan-page.rtl-mode .brand > div{text-align:right;}


  footer{
    display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px;
    padding:14px 32px;
    background:var(--header-bg);
    border-top:1px solid var(--line);
    font-size:12px;color:var(--text-dim);
  }

  footer span{display:flex;align-items:center;gap:6px;}
  /* ---------- home button ---------- */

  .home-btn{
    display:flex;align-items:center;gap:6px;
    padding:7px 14px;border-radius:999px;
    border:1px solid var(--line);
    background:rgba(255,255,255,.04);
    color:var(--text-dim);
    font-size:12.5px;font-weight:700;
    cursor:pointer;white-space:nowrap;
    transition:background .15s ease, color .15s ease, border-color .15s ease;
    font-family:inherit;
  }

  .home-btn:hover{background:rgba(255,255,255,.09);color:var(--text);border-color:var(--accent);}

  /* Theme toggle / export / import / reset-all reuse .home-btn styling as-is
     so nothing about the header's look changes — these are just more pills. */

  .header-actions{flex-wrap:wrap;row-gap:8px;}

  .file-import-label{
    display:flex;align-items:center;gap:6px;
    padding:7px 14px;border-radius:999px;
    border:1px solid var(--line);
    background:rgba(255,255,255,.04);
    color:var(--text-dim);
    font-size:12.5px;font-weight:700;
    cursor:pointer;white-space:nowrap;
    transition:background .15s ease, color .15s ease, border-color .15s ease;
  }

  .file-import-label:hover{border-color:var(--accent);color:var(--text);}

  .file-import-label input{display:none;}
  /* ================= DEVELOPER MODE ================= */

  .dev-link{
    background:none;border:none;color:var(--text-dim);font-size:11px;
    cursor:pointer;font-family:inherit;padding:0;opacity:.55;text-decoration:none;
  }

  .dev-link:hover{opacity:1;color:var(--accent);}

  /* ==========================================================================
     4. HOME PAGE — the major-picker landing screen: intro text, plan cards,
     the "+ New Plan" card, home footer
     ========================================================================== */
  /* ---------- homepage ---------- */

  .home-wrap{max-width:1100px;margin:0 auto;padding:20px;}

  .home-header{
    background:linear-gradient(120deg, var(--header-bg), var(--surface-bg));
    border:1px solid var(--line);
    border-radius:16px;
    padding:22px 28px;
    display:flex;align-items:center;gap:18px;
    margin-bottom:22px;
  }

  .home-header-mascot{
    width:96px;height:96px;object-fit:contain;flex:0 0 auto;border-radius:16px;
  }

  .home-header-body{flex:1 1 auto;min-width:0;}

  .home-header h1{margin:0;font-size:20px;font-weight:800;letter-spacing:.02em;}
  /* The app's name, with the Arabic tagline riding beside it. Baseline-aligned
     rather than centred, so the two read as one lockup instead of two words
     that happen to be next to each other. */
  .home-wordmark{
    display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;
    font-size:23px;font-weight:900;letter-spacing:.09em;
  }
  .home-wordmark-ar{
    font-size:15px;font-weight:700;letter-spacing:.02em;color:var(--accent);
    direction:rtl;
  }

  .home-header p{margin:4px 0 0;font-size:13px;color:var(--text-dim);}

  .home-header-stats{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;}

  .home-header-stats span{
    font-size:11px;font-weight:700;color:var(--text);
    background:rgba(255,255,255,.05);
    border:1px solid var(--line);
    border-radius:999px;
    padding:5px 12px;
    white-space:nowrap;
  }

  .home-header .ar{
    margin-left:auto;text-align:right;direction:rtl;flex:0 0 auto;
  }

  .home-header .ar .ar1{font-size:15px;font-weight:700;}

  .home-header .ar .ar2{font-size:11.5px;color:var(--text-dim);margin-top:2px;}


  .home-intro{
    font-size:14px;color:var(--text-dim);
    margin:0 4px 20px;
    line-height:1.6;
  }

  .home-intro strong{color:var(--text);}

  /* "Continue where you left off" card — only shown on the top-level
     university step when there's a previously-opened plan to resume. */
  .home-resume{
    display:flex;align-items:center;gap:14px;
    background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 14%, transparent), color-mix(in srgb, var(--accent) 5%, transparent));
    border:1px solid var(--accent);
    border-radius:14px;
    padding:14px 16px;
    margin:0 4px 18px;
    cursor:pointer;
    transition:border-color .15s ease, box-shadow .15s ease, transform .15s ease;
  }
  .home-resume:hover, .home-resume:focus-visible{
    box-shadow:0 0 20px color-mix(in srgb, var(--accent) 18%, transparent);
    transform:translateY(-1px);
    outline:none;
  }
  .home-resume .hr-icon{
    font-size:26px;flex:0 0 auto;width:46px;height:46px;border-radius:11px;
    background:color-mix(in srgb, var(--accent) 14%, transparent);display:flex;align-items:center;justify-content:center;
  }
  .home-resume .hr-body{flex:1 1 auto;min-width:0;}
  .home-resume .hr-kicker{font-size:11px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--accent);}
  .home-resume .hr-name{font-size:15px;font-weight:800;color:var(--text);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .home-resume .hr-sub{font-size:11.5px;color:var(--text-dim);margin-top:3px;}
  .home-resume .hr-go{flex:0 0 auto;font-size:20px;color:var(--accent);}
  .home-resume .hr-progress{margin-top:6px;height:5px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden;}
  .home-resume .hr-progress > span{display:block;height:100%;background:var(--accent);border-radius:3px;}


  .plan-grid{
    display:grid;
    grid-template-columns:repeat(2,1fr);
    gap:18px;
  }

  .plan-card{
    background:var(--panel);
    border:1px solid var(--line);
    border-radius:12px;
    padding:22px 22px 20px;
    cursor:pointer;
    text-align:left;
    transition:transform .15s ease, box-shadow .15s ease, border-color .15s ease;
    display:flex;flex-direction:column;gap:12px;
    min-width:0;
  }

  .plan-card:hover{
    transform:translateY(-3px);
    border-color:var(--accent);
    box-shadow:0 10px 26px rgba(0,0,0,.35);
  }

  .plan-card .pc-icon{
    width:52px;height:52px;border-radius:12px;
    background:color-mix(in srgb, var(--accent) 12%, transparent);
    display:flex;align-items:center;justify-content:center;
  }

  .plan-card .pc-icon svg{width:30px;height:30px;}

  .plan-card h2{margin:0;font-size:16px;font-weight:800;line-height:1.3;}

  .plan-card h2 em{color:var(--accent);font-style:normal;display:block;font-size:14px;margin-top:2px;}

  .plan-card p{margin:0;font-size:12.5px;color:var(--text-dim);line-height:1.55;}

  /* A plan's bio can be a full paragraph (credit-hour breakdown, source
     caveats) — same clamp-to-3-lines treatment .pc-uni-desc already uses for
     a university's own description, so a long bio doesn't blow out the card
     and bury the "View plans" CTA below the fold. */
  /* One line. Three lines of a bio on a card in a grid of seventy is a wall
     of dense grey nobody reads from outside the plan — the full text is on
     the plan's own page, which is where someone who wants it already is. */
  .plan-card p.pc-bio{
    display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden;
  }

  .plan-card .pc-cta{
    margin-top:auto;
    font-size:12px;font-weight:700;color:var(--accent);
    display:flex;align-items:center;gap:6px;
  }

  /* A faculty tile is one row: the faculty's icon, its name, and how many
     plans sit behind it. The stacked .plan-card layout above — icon on its
     own line, then paragraphs, then a CTA — is for tiles that have something
     to say. A signpost doesn't, so it gets a row and stays short. */
  .plan-card-faculty{
    flex-direction:row;
    align-items:center;
    gap:14px;
    padding:16px 18px;
  }

  .plan-card-faculty .pc-icon{
    width:44px;height:44px;border-radius:10px;flex:0 0 auto;
  }

  .plan-card-faculty .pc-icon svg{width:26px;height:26px;}

  .pc-faculty-body{min-width:0;display:flex;flex-direction:column;gap:2px;}
  .pc-faculty-body h2{font-size:15px;}
  .pc-faculty-body p{font-size:12px;}

  /* The count is a number, not a sentence — "8 plans" spelled out is exactly
     the kind of line this tile stopped carrying. The words are on the tile's
     aria-label for a screen reader and on this badge's title for a hover. */
  .pc-faculty-count{
    flex:0 0 auto;margin-inline-start:auto;
    min-width:30px;padding:3px 9px;border-radius:999px;
    background:color-mix(in srgb, var(--accent) 12%, transparent);color:var(--accent);
    font-size:13px;font-weight:800;text-align:center;
    font-variant-numeric:tabular-nums;
  }

  .pc-faculty-count-none{background:rgba(255,255,255,.05);color:var(--text-dim);}

  /* The faculty screen's "Add a plan" tile shares a grid row with the faculty
     tiles, so its 150px column height would stretch every row beside it back
     to the size the tiles just stopped being. On this screen only, it becomes
     the same row shape they are. */
  #homeCollegeGrid .new-plan-card{
    flex-direction:row;justify-content:flex-start;gap:14px;
    min-height:0;padding:16px 18px;
  }

  #homeCollegeGrid .new-plan-card .npc-plus{
    width:44px;height:44px;border-radius:10px;border-width:1px;font-size:24px;
  }

  #homeCollegeGrid .new-plan-card .npc-label{text-align:left;}


  .home-footer{
    display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px;
    padding:16px 4px 4px;
    color:var(--text-dim);font-size:12px;
    border-top:1px solid var(--line);
    margin-top:24px;
  }


  @media (max-width:900px){
    .plan-grid{grid-template-columns:1fr;}
    .home-header .ar{display:none;}
    .home-header{padding:16px 18px;gap:12px;}
    .home-header-mascot{width:56px;height:56px;border-radius:12px;}
    .home-header-stats span{font-size:10px;padding:4px 9px;}
  }

  /* Phone-width screens: .home-wrap's 20px padding plus .home-header's own
     16-18px on top of it compounded into ~35-40px of dead space per side —
     18-20% of a typical phone's width. Tighten both so the header, search
     bar, and plan/college cards reach close to the screen edges instead. */
  @media (max-width:480px){
    .home-wrap{padding:10px;}
    .home-header{padding:14px;border-radius:12px;gap:10px;}
    .search-wrap{margin-left:0;margin-right:0;}
    .plan-grid{gap:10px;}
    .plan-card{padding:16px 14px 14px;}
    .plan-card-faculty,
    #homeCollegeGrid .new-plan-card{padding:14px;gap:12px;}
  }

  /* A resting border only earns its keep on a device with hover to promote
     it to something brighter — on a phone .plan-card just sat there as a
     thin outline, since :hover's lift/glow below almost never fires on
     touch. Elevation reads as "tappable" without needing a hover state to
     prove it, same reasoning as every other card in this redesign. */
  @media (max-width:720px){
    .plan-card{border:none; box-shadow:0 4px 14px rgba(0,0,0,.22);}
  }

  .plan-card{position:relative;}

  .imp-origin-badge{
    position:absolute; top:10px; right:14px; z-index:5;
    font-size:10px; font-weight:700; color:var(--text-dim);
    background:rgba(255,255,255,.06); border-radius:999px; padding:2px 9px;
  }
  .new-plan-card{
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px;
    background:var(--panel); border:2px dashed var(--line); border-radius:12px;
    min-height:150px; cursor:pointer; transition:border-color .2s ease, background .2s ease;
    padding:16px;
  }

  .new-plan-card:hover{border-color:var(--accent); background:color-mix(in srgb, var(--accent) 5%, transparent);}

  .new-plan-card .npc-plus{
    width:56px; height:56px; border-radius:50%;
    background:color-mix(in srgb, var(--accent) 12%, transparent); border:2px solid var(--accent); color:var(--accent);
    display:flex; align-items:center; justify-content:center; font-size:30px; font-weight:300;
  }

  .new-plan-card .npc-label{margin:0; font-size:12.5px; color:var(--text-dim); text-align:center;}

  /* ---------- University → College → Plan drill-down ---------- */
  .home-breadcrumb{
    display:flex; align-items:center; flex-wrap:wrap; gap:6px;
    font-size:12.5px; color:var(--text-dim); margin:0 4px 16px;
  }
  .home-breadcrumb button{
    background:none; border:none; color:var(--accent); font-family:inherit;
    font-size:12.5px; font-weight:700; cursor:pointer; padding:2px 0;
  }
  .home-breadcrumb button:hover{text-decoration:underline;}
  .home-breadcrumb .hb-sep{opacity:.5;}
  .home-breadcrumb .hb-current{color:var(--text); font-weight:700;}

  .home-tile-badge{
    position:absolute; top:10px; right:14px; z-index:5;
    font-size:10px; font-weight:700; color:var(--text-dim);
    background:rgba(255,255,255,.06); border-radius:999px; padding:2px 9px;
  }
  .home-tile-empty{
    font-size:11px; color:var(--text-dim); opacity:.7; font-style:italic;
  }
  .pc-major-list{margin:-2px 0 0;}
  .pc-major-row{
    display:flex; justify-content:space-between; align-items:center; gap:8px;
    padding:7px 0; border-top:1px solid var(--line); font-size:12px;
  }
  .pc-major-name{color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; flex:1 1 auto;}
  .pc-major-cr{color:var(--text-dim); font-size:10.5px; white-space:nowrap; flex:none;}
  .pc-major-more{font-size:11px; color:var(--accent); font-weight:700; padding-top:6px;}
  html[dir="rtl"] .pc-major-row, .plan-page.rtl-mode .pc-major-row{direction:rtl;}
  .home-step-empty-note{
    grid-column:1 / -1; font-size:13px; color:var(--text-dim);
    text-align:center; padding:30px 10px; border:1px dashed var(--line);
    border-radius:12px;
  }

  /* ==========================================================================
     5. PAGE LAYOUT — the four-year course grid skeleton: year rows, semester
     blocks, the summer/community-service/internship row, elective pool row
     ========================================================================== */
  /* ---------- RTL (Arabic) mode ---------- */

  .plan-page.rtl-mode{direction:rtl;}

  .plan-page.rtl-mode .sem-label,
  .plan-page.rtl-mode .pool-note,
  .plan-page.rtl-mode .notes li,
  .plan-page.rtl-mode .highlight p,
  .plan-page.rtl-mode .highlight h3{text-align:right;}

  .plan-page.rtl-mode .year-badge{direction:ltr;}
  /* ---------- body layout ---------- */

  .body-grid{
    display:grid;
    grid-template-columns:1fr 300px;
    gap:18px;
    padding:16px 32px 28px;
  }

  /* Imported/custom plans use a simpler single-column wrapper (no sidebar
     grid) around the same .years content .body-grid wraps for built-in
     majors — same horizontal padding, so both get the same mobile
     treatment below via one shared media-query rule. */
  .imp-body-pad{padding:20px 32px;}


  .years{display:flex;flex-direction:column;gap:16px;position:relative;}


  .year-row{
    display:grid;
    grid-template-columns:78px 1fr;
    gap:14px;
    background:var(--header-bg);
    border:1px solid var(--line);
    border-radius:10px;
    padding:14px;
  }

  .year-badge{
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:6px;
    text-align:center;
  }

  .year-badge .num{
    width:38px;height:38px;border-radius:9px;
    background:var(--accent);
    color:var(--on-accent);
    font-weight:800;font-size:17px;
    display:flex;align-items:center;justify-content:center;
    transform:rotate(45deg);
  }

  .year-badge .num span{transform:rotate(-45deg);}

  .year-badge .label{font-size:11.5px;font-weight:700;letter-spacing:.04em;color:var(--text);text-transform:uppercase;}


  .semesters{display:flex;flex-direction:column;gap:10px;}

  .sem{display:flex;flex-direction:column;gap:6px;}

  .sem-label{
    font-size:10.5px;font-weight:700;color:var(--text-dim);
    text-transform:uppercase;letter-spacing:.05em;
    padding-left:2px;
  }

  .course-row{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
    align-items:stretch;
  }

  .divider{border-top:1px dashed var(--line);margin:2px 0;}
  /* special row: summer / community / internship */

  .special-row{
    display:flex;gap:14px;flex-wrap:wrap;
    background:var(--header-bg);
    border:1px solid var(--line);
    border-radius:10px;
    padding:14px 20px;
  }

  .pill{
    display:flex;align-items:center;gap:10px;
    padding:10px 18px;
    border-radius:999px;
    font-size:13px;font-weight:700;
    flex:1 1 200px;
    justify-content:center;
  }

  .pill .ic{font-size:15px;}

  .pill.summer{background:var(--summer);color:var(--summer-tx);}

  .pill.community{background:var(--skills);color:var(--skills-tx);}

  .pill.internship{background:var(--intern);color:var(--intern-tx);}
  /* department electives pool */

  .electives-row{
    background:var(--header-bg);
    border:1px solid var(--line);
    border-radius:10px;
    padding:14px;
  }

  .electives-row .sem-label{padding-left:4px;}

  .electives-row .pool-note{
    font-size:11.5px;
    color:var(--text-dim);
    margin:2px 0 10px 4px;
  }


  @media (max-width:980px){
    .body-grid{grid-template-columns:1fr;}
    .year-row{grid-template-columns:1fr;}
    .year-badge{flex-direction:row;}
    .year-badge .num{transform:none;}
    .year-badge .num span{transform:none;}
  }

  .plan-page{display:none;}

  /* .editing (not .plan-page.editing specifically) so this applies to both
     the built-in majors (.plan-page.editing) AND imported plans, whose
     root uses .sheet.editing instead — same drag mechanics, different
     wrapper class. */
  .editing .course-row{
    transition:background .2s ease, box-shadow .2s ease;
    border-radius:8px;
  }

  .editing .course-row.drop-ok{
    background:rgba(47,191,113,.12);
    box-shadow:inset 0 0 0 2px var(--unlock);
  }

  .editing .course-row.drop-bad{
    background:rgba(224,83,140,.12);
    box-shadow:inset 0 0 0 2px var(--prereq);
  }

  /* ==========================================================================
     6. SEARCH — the course search box, dropdown results, and its highlight/trace
     animation (kept separate from the hover-trace animation in section 7,
     even though they share a similar visual language, since they are
     triggered by two different features)
     ========================================================================== */
  /* The "sketch" effect: bindHover() measures each path's real length with
     getTotalLength() and sets BOTH the hidden starting point (dasharray =
     dashoffset = length, one dash spanning the whole path, offset so none
     of it shows) AND, one frame later, the fully-drawn target (dashoffset
     = 0) — both set inline via JS rather than leaving the target to a CSS
     class. An inline style always wins over a class on specificity, so a
     class-only "stroke-dashoffset:0" would be silently ignored the moment
     any inline value is already sitting on the element; only the
     transition/easing itself is left to CSS here. A fixed CSS dasharray
     value can't replace this either, since edges vary a lot in length
     across the grid.
  */

  @media (prefers-reduced-motion:reduce){
    .connector-layer path.edge.active,
    .connector-layer path.edge.search-active{ transition:opacity .15s ease; stroke-dashoffset:0 !important; }
  }

  .search-wrap{position:relative;margin:0 4px 20px;}

  .course-search-wrap{position:relative;margin:14px 32px 0;}

  .search-box{
    display:flex;align-items:center;gap:10px;
    background:var(--search-bg);
    border:1px solid var(--line);
    border-radius:12px;
    padding:12px 16px;
    transition:border-color .15s ease, box-shadow .15s ease;
  }

  .search-box:focus-within{
    border-color:var(--accent);
    box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);
  }

  .search-ic{font-size:15px;opacity:.75;flex:0 0 auto;}

  .search-input{
    flex:1 1 auto;
    background:transparent;
    border:none;outline:none;
    color:var(--text);
    font-size:13.5px;
    font-family:inherit;
  }

  .search-input::placeholder{color:var(--text-dim);}

  .search-clear{
    display:none;
    flex:0 0 auto;
    background:none;border:none;color:var(--text-dim);
    font-size:16px;cursor:pointer;line-height:1;padding:2px 4px;
  }

  .search-clear:hover{color:var(--text);}

  .search-box.has-value .search-clear{display:block;}


  .search-dropdown{
    position:absolute;
    left:0;right:0;top:calc(100% + 8px);
    background:var(--panel);
    border:1px solid var(--line);
    border-radius:10px;
    box-shadow:0 16px 40px rgba(0,0,0,.45);
    max-height:340px;
    overflow-y:auto;
    z-index:60;
    display:none;
    padding:6px;
  }

  .search-dropdown.open{display:block;}

  .tour-mark{
    display:inline-flex; align-items:center; justify-content:center; width:14px; height:14px;
    border-radius:50%; font-size:10px; line-height:1; color:var(--text-dim); border:1.3px solid var(--line);
  }
  .tour-mark.done{ color:var(--unlock); border-color:var(--unlock); }

  .search-hit{
    display:flex;flex-direction:column;gap:2px;
    padding:9px 12px;
    border-radius:7px;
    cursor:pointer;
    transition:background .1s ease;
  }

  .search-hit:hover, .search-hit.active-hit{background:color-mix(in srgb, var(--accent) 12%, transparent);}

  .search-hit .sh-en{font-size:13px;font-weight:700;color:var(--text);}

  .search-hit .sh-ar{font-size:11.5px;color:var(--text-dim);direction:rtl;text-align:left;}

  .search-hit .sh-meta{
    font-size:10.5px;color:var(--accent);font-weight:700;letter-spacing:.03em;
    display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:2px;
  }
  .search-hit .sh-code{font-variant-numeric:tabular-nums;}

  /* Where the course sits in the plan, and — only when it is worth a word —
     how it stands. Dim rather than accent-coloured: it is context for the
     name above it, not a second thing to read. */
  .search-hit .sh-where{color:var(--text-dim);font-weight:600;}
  .search-hit .sh-state{
    font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
    border-radius:999px;padding:1px 7px;
    color:var(--text-dim);background:rgba(255,255,255,.06);
  }
  .search-hit .sh-state-done{color:var(--unlock);}
  .search-hit .sh-state-locked{color:var(--prereq);}

  /* Why this row is here. Only shown when the marked text cannot say so on
     its own — a code match, an Arabic-name match, or a typo-tolerant one.
     A plain name match gets no label: the mark in the name is the answer. */
  .search-hit .sh-why{
    font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
    color:var(--text-dim);background:rgba(255,255,255,.06);
    border-radius:999px;padding:1px 7px;
  }
  /* A fuzzy hit is the one a student should read before trusting, and
     "closest match" says so in words. It deliberately gets no colour of its
     own: the app ships six themes, three of them light, and a chip tinted
     for one of them is either invisible or shouting on the others. */
  .search-hit .sh-why-fuzzy{font-style:italic;}

  .search-hit .sh-mark{
    background:color-mix(in srgb, var(--accent) 22%, transparent);color:inherit;
    border-radius:3px;padding:0 1px;font-weight:inherit;
  }

  .search-empty{padding:14px 12px;font-size:12.5px;color:var(--text-dim);text-align:center;}


  .plan-page.rtl-mode .course-search-wrap{direction:rtl;}

  .plan-page.rtl-mode .search-hit .sh-ar{text-align:right;}
  /* ================= course highlight states ================= */

  .course.search-selected{
    box-shadow:0 0 0 3px var(--accent), 0 8px 20px rgba(0,0,0,.45) !important;
    animation:searchPulse 1s ease-out 2;
    z-index:8;
  }

  .course.search-prereq{
    box-shadow:0 0 0 3px var(--prereq), 0 6px 16px rgba(0,0,0,.4) !important;
    z-index:7;
  }

  .course.search-unlock{
    box-shadow:0 0 0 3px var(--unlock), 0 6px 16px rgba(0,0,0,.4) !important;
    z-index:7;
  }

  @keyframes searchPulse{
    0%{transform:scale(1);}
    30%{transform:scale(1.08);}
    60%{transform:scale(0.99);}
    100%{transform:scale(1);}
  }

  .search-badge{
    position:absolute;
    top:-9px;
    font-size:10px;
    font-weight:800;
    padding:2px 6px;
    border-radius:999px;
    white-space:nowrap;
    box-shadow:0 3px 8px rgba(0,0,0,.4);
    z-index:9;
    pointer-events:none;
  }

  .course.search-prereq .search-badge{left:-6px;background:var(--prereq);color:#fff;}

  .course.search-unlock .search-badge{right:-6px;background:var(--unlock);color:var(--navy-900);}

  .plan-page.rtl-mode .course.search-prereq .search-badge{left:auto;right:-6px;}

  .plan-page.rtl-mode .course.search-unlock .search-badge{right:auto;left:-6px;}


  .connector-layer path.edge.search-active{
    stroke-width:3.4;
    opacity:1;
    stroke:#ff6fb8;
    /* Single glow, not a stack of three — see path.edge.active above for
       why (stacked drop-shadows go invisible on some PC GPUs). */
    filter: drop-shadow(0 0 5px rgba(224,83,140,.85));
    transition:
      opacity .3s cubic-bezier(.4,0,.2,1),
      stroke-width .3s cubic-bezier(.4,0,.2,1),
      filter .3s cubic-bezier(.4,0,.2,1),
      stroke .3s cubic-bezier(.4,0,.2,1),
      stroke-dashoffset .6s cubic-bezier(.65,0,.35,1);
  }

  .connector-layer path.edge.search-dim{opacity:.04;}


  @media (max-width:640px){
    .course-search-wrap{margin-left:16px;margin-right:16px;}
  }

  /* ==========================================================================
     7. COURSE CARDS, LEGEND & PREREQUISITE ARROWS — the course card itself
     (including its checkbox, category color classes, badges, drag states),
     the legend, and the SVG connector/arrow system with its draw-in animation
     ========================================================================== */

  .plan-page.rtl-mode .legend .item,

  .plan-page.rtl-mode .legend .chip{margin-right:0;margin-left:7px;}
  /* ---------- legend ---------- */

  .legend{
    margin:18px 32px 6px;
    background:var(--panel);
    border:1px solid var(--line);
    border-radius:8px;
    padding:12px 20px;
    display:flex;
    flex-wrap:wrap;
    gap:22px;
    align-items:center;
    font-size:12.5px;
  }

  .legend .chip{width:11px;height:11px;border-radius:3px;display:inline-block;margin-right:6px;vertical-align:-1px;}
  [dir="rtl"] .legend .chip{margin-right:0;margin-left:6px;}

  .legend .chip.pair-chip{background:rgba(224,168,63,.18);border:1px dashed var(--connect);}

  .legend .item{display:flex;align-items:center;color:var(--text-dim);}

  /* connector arrows (prerequisites), drawn by JS to stay accurate as boxes wrap/reflow */

  .connector-layer{
    position:absolute;
    top:0;left:0;
    overflow:visible;
    pointer-events:none;
    z-index:5;
  }

  .connector-layer path.edge{
    fill:none;
    stroke:var(--prereq);
    stroke-width:1.3;
    /* Idle state: invisible until a course is traced (hover or hold), which
       is what actually shows its chain, at full opacity / thicker stroke /
       brighter pink / glow via .active below. */
    opacity:0;
    transition:opacity .5s cubic-bezier(.4,0,.2,1), stroke-width .35s cubic-bezier(.4,0,.2,1), filter .35s cubic-bezier(.4,0,.2,1), stroke .35s cubic-bezier(.4,0,.2,1);
  }

  .connector-layer path.edge.dim{opacity:.04;}

  .connector-layer path.edge.active{
    stroke-width:3.4;
    opacity:1;
    stroke:#ff6fb8;
    /* A single drop-shadow, not a stack of three. Stacked drop-shadow
       filters on an SVG stroke render the whole path INVISIBLE on some
       PC GPUs/drivers (the line animates but paints nothing), while
       phones show it fine — which is exactly the "lines move but are
       invisible on PC" report. One light glow keeps the look and is far
       less likely to trip that compositing bug; the bright thick stroke
       itself stays visible even if the glow is dropped entirely. */
    filter: drop-shadow(0 0 5px rgba(224,83,140,.85));
    transition:
      opacity .3s cubic-bezier(.4,0,.2,1),
      stroke-width .3s cubic-bezier(.4,0,.2,1),
      filter .3s cubic-bezier(.4,0,.2,1),
      stroke .3s cubic-bezier(.4,0,.2,1),
      stroke-dashoffset .6s cubic-bezier(.65,0,.35,1);
  }

  [id$="-prereqArrow"] path{fill:var(--prereq);opacity:.5;transition:opacity .35s cubic-bezier(.4,0,.2,1), fill .35s cubic-bezier(.4,0,.2,1);}

  [id$="-prereqArrowActive"] path{fill:#ff6fb8;}


  .course{
    flex:1 1 130px;
    min-width:120px;
    max-width:190px;
    border-radius:8px;
    padding:9px 10px 8px;
    position:relative;
    border:1px solid rgba(255,255,255,.08);
    transition:transform .12s ease, box-shadow .12s ease;
    cursor:pointer;
    /* The hold-to-trace gesture is a text-length press on a text-filled
       card, which is exactly what a phone's own "select this text" long-
       press callout is also listening for — without this it wins the race
       and pops a Copy/Share bubble mid-hold. Course names aren't meant to
       be copied out of the app anyway, so selection is off entirely here
       (touch-action is deliberately left at its default, unlike the
       .editing drag state below, so scrolling that starts on a course card
       still works normally). */
    -webkit-user-select:none;
    user-select:none;
    -webkit-touch-callout:none;
    /* --card-scale is the reading-size preference (Settings -> Preferences);
       it defaults to 1, so nothing moves until a student chooses otherwise. */
    min-height:calc(58px * var(--card-scale, 1));
  }

  .course:hover{transform:translateY(-2px);}

  .course.node-active{
    box-shadow:0 0 0 2px #ff6fb8, 0 0 16px rgba(255,111,184,.55), 0 6px 14px rgba(0,0,0,.35);
    transition:box-shadow .35s cubic-bezier(.4,0,.2,1);
  }

  /* The tick badge sits in the top-right corner ON TOP of the card, so a
     long course name ran underneath it — "Beginning English" read as
     "Beginning Englis". Keep the first line clear of it. */
  .course .name{
    font-size:calc(12.5px * var(--card-scale, 1));font-weight:700;line-height:1.28;
    padding-right:24px;
  }
  .plan-page.rtl-mode .course .name,
  .sheet.rtl-mode .course .name{padding-right:0;padding-left:24px;}
  /* Edit mode puts TWO buttons in the corner (rename, remove) where the plan
     puts one tick, and this rule was zeroing the padding that keeps the title
     clear of them — so "Community Service" read with a pen through it. Wider
     than the plan's 24px, because there are two of them. */
  .editing .course .name{padding-right:46px;padding-left:0;}
  /* .editing and .rtl-mode land on the SAME element (the plan sheet), so
     these are compounded, not nested — and they need one class more than the
     narrow-width block's own `.sheet.rtl-mode .course .name{padding:0}`,
     which is later in the file and otherwise wins the tie. */
  .sheet.rtl-mode.editing .course .name,
  .plan-page.rtl-mode.editing .course .name{padding-right:0;padding-left:46px;}
  /* ---------- My Progress: completion checkbox on each course ---------- */

  /* The circle itself lives on ::before, sized to the content box only —
     .course-check's padding is an invisible tap-target extension (44px on
     phones, per touch guidelines), not part of the visible badge. Padding
     used to inflate the drawn circle right along with the tap target,
     because a filled background with border-radius paints over padding by
     default: a 44px hit area meant a 44px green circle sitting on top of
     the card instead of a small tick in the corner. */
  .course-check{
    position:absolute;
    top:5px; right:5px;
    width:17px; height:17px;
    display:flex; align-items:center; justify-content:center;
    /* The tick is an SVG on currentColor, so "colour it transparent" is
       still how an unticked box hides its mark — same trick as when this
       was the ✓ character, and it keeps the box's own ::before circle. */
    font-size:11px; line-height:1; color:transparent;
    cursor:pointer;
    z-index:3;
    padding:5px;
    margin:-5px;
    box-sizing:content-box;
  }
  .course-check::before{
    content:'';
    position:absolute;
    inset:5px;
    border-radius:50%;
    border:1.5px solid rgba(255,255,255,.5);
    background:rgba(0,0,0,.2);
    transition:border-color .12s ease,background .12s ease;
    z-index:-1;   /* .course-check is its own stacking context (z-index:3),
                     so this only drops below the ✓ glyph, not the whole card */
  }

  .course-check:hover::before{border-color:#fff;}

  .course-check:focus-visible{outline:2px solid var(--accent); outline-offset:1px;}

  .course.completed .course-check{color:var(--on-accent);}
  .course.completed .course-check::before{
    background:#2ecc71; border-color:#2ecc71;
  }

  /* A finished course used to be dimmed to 50% AND struck through AND
     badged — three signals stacked, which pushed the name and hour line to
     2.8-3.7:1 contrast (AA asks 4.5:1) and made a completed plan genuinely
     hard to read. One signal is enough: the solid green tick. The card keeps
     a slightly softer surface so the eye still separates done from not, and
     the text stays at full strength. */
  .course.completed{opacity:.86;}

  .course.retake-course{box-shadow:0 0 0 2px var(--prereq) inset;}
  .retake-badge{
    display:block; font-size:9.5px; font-weight:800; color:#ff9ecb;
    background:rgba(224,83,140,.18); border-radius:5px; padding:1px 6px;
    margin-bottom:4px; width:fit-content;
  }

  .course.completed .name{text-decoration:none;}

  .plan-page.rtl-mode .course-check,
  .sheet.rtl-mode .course-check{right:auto; left:5px; margin:-5px;}
  /* ---------- My Progress: status badges (Completed / Available) ---------- */

  .course-badge{
    display:none;
    margin-top:5px;
    font-size:9px;
    font-weight:800;
    letter-spacing:.05em;
    text-transform:uppercase;
    padding:2px 6px;
    border-radius:3px;
    width:fit-content;
    line-height:1.4;
  }

  .course-badge.badge-completed{
    background:rgba(46,204,113,.16);
    border:1px solid #2ecc71;
    color:#7be8ab;
  }

  .course-badge.badge-available{
    background:color-mix(in srgb, var(--accent) 14%, transparent);
    border:1px solid var(--accent);
    color:var(--accent);
  }
  /* ---------- My Progress: newly-unlocked courses get a subtle glow ---------- */

  .course.available{
    box-shadow:0 0 0 1.5px var(--accent), 0 0 9px color-mix(in srgb, var(--accent) 40%, transparent);
    animation:course-available-pulse 2.6s ease-in-out infinite;
  }

  .course.available:hover{box-shadow:0 0 0 1.5px var(--accent), 0 0 15px color-mix(in srgb, var(--accent) 65%, transparent);}

  @keyframes course-available-pulse{
    0%,100%{box-shadow:0 0 0 1.5px var(--accent), 0 0 7px color-mix(in srgb, var(--accent) 35%, transparent);}
    50%{box-shadow:0 0 0 1.5px var(--accent), 0 0 14px color-mix(in srgb, var(--accent) 65%, transparent);}
  }

  @media (prefers-reduced-motion:reduce){
    .course.available{animation:none;}
  }
  /* paired lecture + lab: shaded shared box with a small connector glyph between them */

  .pair-group{
    display:flex;
    align-items:stretch;
    gap:4px;
    padding:6px;
    border-radius:12px;
    border:1px dashed var(--connect);
    background:rgba(224,168,63,.07);
    flex:1 1 270px;
    max-width:400px;
  }

  .pair-group .course{flex:1 1 120px;max-width:none;min-width:100px;}

  .pair-group .connect-glyph{
    align-self:center;
    color:var(--connect);
    font-size:13px;
    font-weight:700;
    flex:0 0 auto;
    padding:0 2px;
  }

  /* The connect-glyph is now a real toggle button (was a static span) —
     strip default button chrome and add hover/press feedback so it reads
     as clickable without disturbing its original inline placement between
     the two paired cards. */
  button.connect-glyph{
    background:none; border:none; cursor:pointer; font-size:15px;
    border-radius:6px; transition:background .15s ease, transform .1s ease;
  }
  button.connect-glyph:hover{background:rgba(224,168,63,.18);}
  button.connect-glyph:active{transform:scale(0.92);}

  /* Removing a course can leave its pair-group wrapper (the dashed
     "course + lab" box) with fewer than 2 courses left inside — an empty
     or near-empty dashed box otherwise sits there with nothing useful in
     it. __applyRemovedCourses toggles these based on how many of a
     wrapper's courses are still visible: 0 left -> hide the wrapper
     entirely; exactly 1 left -> keep that course but drop the box/border
     styling so it renders like an ordinary standalone card. */
  .pair-group.pg-empty{display:none !important;}
  .pair-group.pg-single{
    border:none; background:none; padding:0; gap:0;
  }
  .pair-group.pg-single .connect-glyph{display:none;}
  /* colors */

  .core{background:var(--core);color:var(--core-tx);}

  .skills{background:var(--skills);color:var(--skills-tx);}

  .math{background:var(--math);color:var(--math-tx);}

  .dept{background:var(--dept);color:var(--dept-tx);}

  .eng{background:var(--eng);color:var(--eng-tx);}

  .uni{background:var(--uni);color:var(--uni-tx);}

  .free{background:var(--free);color:var(--free-tx);}

  .misc{background:var(--misc);color:var(--misc-tx);}

  /* The plan's own blurb, folded. Open it and it reads as it always did. */
  .imp-about{
    margin:0 0 12px; border:1px solid var(--line); border-radius:10px;
    background:var(--panel); overflow:hidden;
  }
  .imp-about > summary{
    list-style:none; cursor:pointer; padding:9px 14px;
    font-size:12.5px; font-weight:700; color:var(--text-dim);
    display:flex; align-items:center; gap:8px;
  }
  .imp-about > summary::-webkit-details-marker{display:none;}
  .imp-about > summary::after{
    content:'›'; margin-inline-start:auto; font-size:16px; line-height:1;
    transition:transform .18s ease;
  }
  .imp-about[open] > summary::after{transform:rotate(90deg);}
  .imp-about > summary:hover{color:var(--text);}
  .imp-about > p{
    margin:0; padding:0 14px 12px; font-size:12px; line-height:1.6;
    color:var(--text-dim);
  }
  @media (prefers-reduced-motion:reduce){ .imp-about > summary::after{transition:none;} }

  /* ---------- one card treatment, every width ----------
     Same plan, two looks: the wide screen filled the whole card with its
     requirement colour, the phone drew a pale panel with a coloured edge.
     One of them had to win everywhere, and the filled one is the one that
     carries the information — the colour IS the requirement bucket, and at
     a glance across a semester that is what you are reading. The pale card
     said the same thing in a 4px line.

     So: filled, at every width, and in edit mode too. Sizing stays
     per-breakpoint; only the look is shared. */
  .course{
    background:var(--misc); color:var(--misc-tx);
    border:none; border-radius:12px;
    box-shadow:0 3px 10px rgba(0,0,0,.2);
  }
  .course.core{background:var(--core); color:var(--core-tx);}
  .course.skills{background:var(--skills); color:var(--skills-tx);}
  .course.math{background:var(--math); color:var(--math-tx);}
  .course.dept{background:var(--dept); color:var(--dept-tx);}
  .course.eng{background:var(--eng); color:var(--eng-tx);}
  .course.uni{background:var(--uni); color:var(--uni-tx);}
  .course.free{background:var(--free); color:var(--free-tx);}
  .course.misc{background:var(--misc); color:var(--misc-tx);}
  .course.intern{background:var(--intern); color:var(--intern-tx);}
  .course.summer{background:var(--summer); color:var(--summer-tx);}

  /* The ten fills above are fixed colours, not theme tokens — they have to
     be, because they mean "College Requirement" and that cannot change with
     the theme. Their ink is therefore fixed too: the --*-tx pairs are each
     chosen against their own fill, so the meta line stays legible on all six
     themes rather than following --text-dim onto a dark red card. */
  .course .course-meta,
  .course.completed .course-meta{color:inherit; opacity:.86;}

  /* A course the student removed from their own plan (e.g. tested out of
     Intermediate English, or placed straight into Advanced). Hidden from the
     grid and skipped by every count via .course[id]:not(.course-removed). */
  .course.course-removed{display:none !important;}

  .modal-remove-block{margin-top:6px;border-top:1px dashed var(--line);padding-top:12px;}
  .course-remove-btn{
    width:100%;padding:9px 12px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:700;
    background:rgba(224,83,140,.12);color:#ff9ecb;border:1px solid var(--prereq);
    transition:background .15s ease;
  }
  .course-remove-btn:hover{background:var(--prereq);color:#fff;}
  .course-remove-btn.is-restore{
    background:rgba(47,191,113,.14);color:#8ff0bd;border-color:var(--unlock);
  }
  .course-remove-btn.is-restore:hover{background:var(--unlock);color:#04150c;}

  .course.jump-highlight{animation:jumpPulse 1s ease-in-out 2;}
  /* ================= COLLAPSIBLE LEGEND (mobile-friendly) ================= */

  /* The legend is a key, so it is swatches: no heading, no "tap to expand",
     no sentence about hovering a course. It carries only the requirement
     buckets the plan in front of you actually uses (js/28-imported.js
     legendHtml), which is why it fits on a row in the first place. */
  .legend{
    padding:10px 20px; display:flex; flex-wrap:wrap; gap:8px 18px;
    align-items:center; font-size:12px;
  }

  @media (max-width:640px){
    .legend{margin:12px 14px 4px; gap:7px 13px; padding:9px 12px;}
  }
  /* Border color/width rather than box-shadow, so it never fights with the
     existing hover/available/active glow states (those are box-shadow based). */

  .course.workload-hard{border-color:var(--prereq);border-width:2px;padding:8px 9px 7px;}

  .course.workload-easy{border-color:var(--unlock);border-width:2px;padding:8px 9px 7px;}

  .course .custom-name-tag{
    display:block;font-size:9.5px;font-style:italic;opacity:.85;
    margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  }

  /* ---------- hold a year to see what is open in it (js/05-collapse-finished-years.js) ----------
     Replaces the old app-wide "Show only what I can take" switch, which was
     a persistent setting answering a momentary question — a student wants
     to know what is open right now, glance, and carry on, not leave a
     filter on and later wonder why half the plan is faded.

     So it is a hold, scoped to the year being held: everything done or
     locked inside that year fades back, and what is actually open stays.
     The prerequisite lines are deliberately left alone, because they are
     what explains WHY a faded course is faded. */
  .year-peek .course:not(.available):not(.completed),
  .year-peek .course.completed{
    opacity:.26;filter:saturate(.5);transition:opacity .15s ease,filter .15s ease;
  }
  .year-peek .course.available:not(.completed){
    box-shadow:0 0 0 2px var(--unlock) inset;
  }
  @media (prefers-reduced-motion:reduce){
    .year-peek .course{transition:none;}
  }

  /* ---------- milestones (js/64-milestones.js) ----------
     Two tick marks added to the progress bar's own track, at the cumulative
     hour where a real category requirement closes out, plus one caption
     line naming the nearer of the two. */
  .pw-gate{
    position:absolute;inset-block:0;width:2px;background:var(--connect);
    pointer-events:none;
  }
  .pw-gate-next{background:var(--accent);}

  /* Degree Audit's two modes — where the GPA is, and where it would be.
     What-if used to be a menu row; it draws into this screen now. */
  .au-modes{
    display:inline-flex;gap:2px;padding:3px;margin:0 0 14px;
    background:rgba(255,255,255,.04);
    border:1px solid var(--line);border-radius:10px;
  }
  .au-mode{
    font-family:inherit;font-size:12.5px;font-weight:700;
    padding:7px 15px;border-radius:8px;border:0;cursor:pointer;
    background:none;color:var(--text-dim);
  }
  .au-mode:hover{color:var(--text);}
  .au-mode-on{background:var(--accent);color:var(--bg);}
  .au-mode:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  /* Rides on the numbers line rather than making a sentence of its own —
     js/64-milestones.js appends it into .pw-num. */
  .pw-gate-caption{
    margin-inline-start:6px;font-size:11.5px;font-weight:600;color:var(--text-dim);
    font-variant-numeric:tabular-nums;
  }
  .pw-num{
    font-size:12.5px;color:var(--text-dim);white-space:nowrap;
    font-variant-numeric:tabular-nums;
  }
  .pw-num b{color:var(--text);font-weight:800;font-size:13.5px;}

  /* A folded year hides its body outright rather than height-animating it:
     the connector SVG is redrawn against the new layout on every toggle
     (js/05-collapse-finished-years.js), and animating the height would mean
     redrawing every prerequisite line on every frame of it. */
  .imp-year-block.year-collapsed > .imp-year-body,
  .year-row.year-collapsed .semesters{display:none;}
  /* Prerequisite connectors used to be hidden outright while collapse mode
     was on (the layer reflows when years fold, and stale lines pointed at
     now-hidden courses). That threw away every line between two courses that
     were still on screen, and pressing "Show ▾" did not bring them back —
     so collapsing one finished year silently disabled prerequisite tracing
     for the whole plan. The layer now stays visible; js/05-collapse-finished-
     years.js redraws it on every fold/unfold and drawConnectors() skips any
     course whose box has collapsed to zero size. */

  /* ---------- per-semester workload roll-up ---------- */
  .sem-workload{
    display:inline-flex;flex-wrap:wrap;gap:6px;margin-left:10px;vertical-align:middle;
  }
  .plan-page.rtl-mode .sem-workload, .sheet.rtl-mode .sem-workload{margin-left:0;margin-right:10px;}
  .sem-workload .swl{
    font-size:10px;font-weight:800;letter-spacing:.02em;
    padding:2px 7px;border-radius:999px;line-height:1.4;white-space:nowrap;
  }
  .sem-workload .swl-hard{background:rgba(224,50,47,.16);color:#ff9a97;}
  .sem-workload .swl-medium{background:color-mix(in srgb, var(--accent) 16%, transparent);color:var(--accent);}
  .sem-workload .swl-easy{background:rgba(74,222,128,.16);color:#8ff0b0;}

  /* ---------- plan overview & print ---------- */
  .po-card{max-width:900px;width:100%;}

  /* A page, not a panel. The preview used to be a dark modal that printed as
     a white sheet, so what you checked before printing was not what came
     out. This is white, page-proportioned and shadowed in every theme —
     including the dark ones — because paper is white wherever you are. */
  .po-sheet{
    background:#fff; color:#11161f;
    padding:clamp(20px, 4vw, 42px);
    border-radius:4px;
    box-shadow:0 18px 44px rgba(0,0,0,.4);
    max-width:820px; margin:0 auto;
  }
  /* Every colour on the sheet, restated for paper. The overview was built
     for a dark modal, so its text is var(--text) — near-white, which on a
     white page is invisible. Each of these is a real rule in the block
     below being answered here, not a blanket inherit. */
  .po-sheet .po-name{color:#11161f;}
  .po-sheet .po-year{background:#fbfcfe; border-color:#dfe4ec;}
  .po-sheet .po-year-label{color:#1b4f8c;}
  .po-sheet .po-courses li{color:#11161f; border-color:#e6e9ef;}
  .po-sheet .po-cname{color:#11161f;}
  .po-sheet .po-uni, .po-sheet .po-sem-label, .po-sheet .po-meta span,
  .po-sheet .po-cr, .po-sheet .po-foot,
  .po-sheet .po-courses li .po-mark{color:#5a6474;}
  .po-sheet .po-grade{background:#eef1f6; color:#11161f;}
  .po-sheet .po-done .po-cname{color:#3c7d55;}
  .po-sheet .po-header{border-color:#d8dde6;}

  /* Full mode: the catalogue number and what a course waits on — the two
     things read off a printout at registration, and the two that make it
     long enough that nobody wants them by default. */
  .po-code{
    display:inline-block; margin-inline-start:7px;
    font-size:10.5px; color:#7b8493; font-variant-numeric:tabular-nums;
  }
  .po-needs{display:block; font-size:10.5px; color:#7b8493; line-height:1.4; margin-top:1px;}

  .po-actions{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
  .po-modes{display:inline-flex; border:1px solid var(--line); border-radius:999px; padding:2px;}
  .po-mode{
    padding:6px 14px; border-radius:999px; border:none; background:none;
    color:var(--text-dim); font-family:inherit; font-size:12px; font-weight:700; cursor:pointer;
  }
  .po-mode.is-on{background:var(--accent); color:var(--on-accent);}
  .po-print{margin-inline-start:auto; border-color:var(--accent); color:var(--text);}
  /* This modal builds its own markup (js/07-plan-overview-print.js) without
     the standard .modal-close + .modal-body pair every other modal uses, so
     it never picked up the shared ".modal-card > .modal-body{overflow-y:
     auto}" scroll rule — a plan with more than a screenful of years/
     semesters (mobile especially, where .modal-card is already height-
     capped) had no way to reach anything past what first fit on screen.
     #planOverviewPrintRoot is this modal's equivalent of .modal-body. */
  #planOverviewPrintRoot{
    overflow-y:auto;
    min-height:0;
    overscroll-behavior:contain;
    padding-inline-end:2px;
  }
  .po-actions{display:flex;justify-content:flex-end;margin-bottom:14px;}
  .po-header{border-bottom:2px solid var(--line);padding-bottom:14px;margin-bottom:16px;}
  .po-title{display:flex;align-items:center;gap:12px;}
  .po-title .po-icon{font-size:30px;}
  .po-title .po-name{font-size:20px;font-weight:800;color:var(--text);}
  .po-title .po-uni{font-size:12px;color:var(--text-dim);margin-top:2px;}
  .po-meta{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:12px;font-size:12.5px;color:var(--text-dim);}
  .po-meta span{font-weight:600;}
  .po-years{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;}
  @media (max-width:640px){ .po-years{grid-template-columns:1fr;} }
  .po-year{border:1px solid var(--line);border-radius:10px;padding:12px 14px;background:var(--panel);}
  .po-year-label{font-size:13px;font-weight:800;color:var(--accent);margin-bottom:8px;text-transform:capitalize;}
  .po-sem{margin-bottom:10px;}
  .po-sem-label{font-size:11px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:.03em;margin-bottom:4px;}
  .po-courses{list-style:none;margin:0;padding:0;}
  .po-courses li{display:flex;align-items:baseline;gap:6px;font-size:12px;padding:2px 0;color:var(--text);}
  .po-courses li .po-mark{color:var(--text-dim);flex:0 0 auto;font-weight:800;}
  .po-courses li.po-done{color:var(--text);}
  .po-courses li.po-done .po-mark{color:var(--unlock);}
  .po-courses li .po-cname{flex:1 1 auto;}
  .po-courses li .po-cr{font-size:10px;color:var(--text-dim);flex:0 0 auto;}
  .po-courses li .po-grade{font-size:10px;font-weight:800;color:var(--accent);flex:0 0 auto;}
  .po-foot{font-size:10.5px;color:var(--text-dim);text-align:center;margin-top:16px;font-style:italic;}

  /* Print: strip all app chrome and show only the overview, in a plain
     high-contrast form that survives a black-and-white printer. */
  @media print{
    body{background:#fff !important;}
    body > *{display:none !important;}
    #planOverviewOverlay{display:block !important;position:static !important;background:#fff !important;padding:0 !important;}
    #planOverviewOverlay .modal-card{box-shadow:none !important;border:none !important;max-width:100% !important;background:#fff !important;color:#000 !important;padding:0 !important;}
    #planOverviewOverlay .po-actions, #planOverviewOverlay .modal-close{display:none !important;}
    /* On paper the sheet IS the paper — drop the shadow and the rounded
       corner it uses to look like one on screen. */
    #planOverviewOverlay .po-sheet{
      box-shadow:none !important; border-radius:0 !important;
      max-width:100% !important; padding:0 !important;
    }
    /* The on-screen scroll fix above (overflow-y:auto, height-capped by the
       modal's own max-height) must not clip the printed page — printing
       needs every year to actually flow across pages, not stop at whatever
       fit in the on-screen scroll box. */
    #planOverviewPrintRoot{color:#000 !important;overflow:visible !important;max-height:none !important;}
    #planOverviewPrintRoot .po-name, #planOverviewPrintRoot .po-year-label, #planOverviewPrintRoot .po-courses li, #planOverviewPrintRoot .po-courses li.po-done{color:#000 !important;}
    #planOverviewPrintRoot .po-uni, #planOverviewPrintRoot .po-meta, #planOverviewPrintRoot .po-sem-label, #planOverviewPrintRoot .po-cr, #planOverviewPrintRoot .po-foot{color:#333 !important;}
    #planOverviewPrintRoot .po-year{border:1px solid #999 !important;background:#fff !important;break-inside:avoid;}
    #planOverviewPrintRoot .po-grade{color:#000 !important;}
    #planOverviewPrintRoot .po-mark{color:#000 !important;}
    #planOverviewPrintRoot .po-header{border-color:#000 !important;}
    @page{margin:1.2cm;}
  }

  .course-drag-ghost{
    position:fixed; pointer-events:none; z-index:2000;
    opacity:.92; transform:rotate(-2deg) scale(1.04);
    box-shadow:0 12px 30px rgba(0,0,0,.5);
    transition:none;
  }

  .course.drag-source-hidden{opacity:.25; filter:grayscale(.6);}

  @keyframes dropShake{
    0%,100%{ transform:translateX(0); }
    20%{ transform:translateX(-6px); }
    40%{ transform:translateX(6px); }
    60%{ transform:translateX(-4px); }
    80%{ transform:translateX(4px); }
  }

  .course.drop-rejected{animation:dropShake .35s ease;}

  /* Mobile course grid — this app is meant to be used on a phone first, not
     as a shrunk-down desktop layout. Smaller, denser cards fit two per row
     instead of the desktop 120-190px range collapsing to one per row at a
     real phone width. (The connector SVG itself is hidden below 720px —
     see the .connector-layer{display:none} rule and its comment further
     down, in the one-column course-row section.) */
  @media (max-width:640px){
    .body-grid, .imp-body-pad{padding:14px 10px 22px;}
    .legend{margin-left:16px;margin-right:16px;padding:10px 14px;gap:14px;}
    .year-row{padding:10px;}
    .course{flex:1 1 44%;min-width:0;max-width:none;padding:7px 8px 6px;}
    .course .name{font-size:11.5px;}
  }

  /* ==========================================================================
     8. SIDEBAR — MY PROGRESS PANEL — the sidebar shell, generic .panel styling,
     and the credit-hour/percentage/electives summary panel
     ========================================================================== */

  .plan-page.rtl-mode .highlight{flex-direction:row-reverse;}
  /* ---------- My Progress: HUD-style header widget ---------- */

  .progress-widget{
    margin:14px 32px 0;
    padding:12px 24px;
    background:
      linear-gradient(180deg, color-mix(in srgb, var(--accent) 7%, transparent), color-mix(in srgb, var(--accent) 0%, transparent) 60%),
      var(--panel);
    border:1px solid var(--line);
    border-radius:4px;
    display:flex;
    align-items:center;
    gap:16px;
    flex-wrap:wrap;
    position:relative;
  }
  /* corner tick brackets, echoing the diamond/angular language used in the year badges */

  .progress-widget::before,
  .progress-widget::after{
    content:"";
    position:absolute;
    top:4px; bottom:4px;
    width:7px;
    border-top:2px solid var(--accent);
    border-bottom:2px solid var(--accent);
    opacity:.5;
    pointer-events:none;
  }

  .progress-widget::before{ left:2px; border-left:2px solid var(--accent); }

  .progress-widget::after{ right:2px; border-right:2px solid var(--accent); }


  .progress-widget .pw-label{
    font-weight:800; font-size:11px; letter-spacing:.09em; text-transform:uppercase;
    color:var(--accent); white-space:nowrap;
    display:flex; align-items:center; gap:8px;
  }

  .progress-widget .pw-label::before{
    content:"";
    width:7px; height:7px; flex:0 0 auto;
    background:var(--accent);
    transform:rotate(45deg);
    box-shadow:0 0 6px var(--accent);
  }


  .progress-widget .pw-track{
    flex:1 1 170px; min-width:120px; height:11px;
    background:rgba(255,255,255,.06);
    border:1px solid var(--line);
    clip-path:polygon(7px 0,100% 0,calc(100% - 7px) 100%,0 100%);
    position:relative;
    overflow:hidden;
  }

  .progress-widget .pw-track::before{
    content:"";
    position:absolute; inset:0;
    background-image:repeating-linear-gradient(90deg, rgba(255,255,255,.14) 0 1px, transparent 1px 13px);
    pointer-events:none;
    z-index:1;
  }

  .progress-widget .pw-fill{
    height:100%; width:0%;
    background-image:
      repeating-linear-gradient(135deg, rgba(255,255,255,.22) 0 5px, transparent 5px 10px),
      linear-gradient(90deg,#2ecc71,var(--accent));
    box-shadow:0 0 9px color-mix(in srgb, var(--accent) 60%, transparent);
    transition:width .35s ease;
  }

  .progress-widget .pw-text{
    font-size:11.5px; color:var(--text-dim); white-space:nowrap;
    font-variant-numeric:tabular-nums;
  }

  .pw-reset{
    border:1px solid var(--line); background:transparent; color:var(--text-dim);
    font-size:10.5px; font-weight:700; letter-spacing:.03em; text-transform:uppercase;
    padding:6px 14px; cursor:pointer; white-space:nowrap;
    font-family:inherit;
    clip-path:polygon(6px 0,100% 0,100% calc(100% - 6px),calc(100% - 6px) 100%,0 100%,0 6px);
  }

  .pw-reset:hover{border-color:var(--accent); color:var(--text); box-shadow:0 0 6px color-mix(in srgb, var(--accent) 35%, transparent);}

  .plan-page.rtl-mode .progress-widget{direction:rtl;}

  @media (max-width:640px){
    .progress-widget{margin:14px 16px 0; padding:12px 18px;}
  }
  /* ---------- sidebar ---------- */

  .sidebar{display:flex;flex-direction:column;gap:16px;}

  .panel{
    background:var(--panel);
    border:1px solid var(--line);
    border-radius:10px;
    padding:18px 20px;
  }

  .panel h2{
    margin:0 0 12px;font-size:14px;font-weight:700;
    border-bottom:1px solid var(--line);padding-bottom:10px;
  }

  .highlight{display:flex;gap:10px;margin-bottom:14px;}

  .highlight:last-child{margin-bottom:0;}

  .highlight .hic{
    width:30px;height:30px;flex:0 0 30px;border-radius:8px;
    background:color-mix(in srgb, var(--accent) 12%, transparent);
    display:flex;align-items:center;justify-content:center;
    color:var(--accent);font-size:15px;
  }

  .highlight h3{margin:0 0 3px;font-size:12.5px;font-weight:700;}

  .highlight p{margin:0;font-size:11.5px;color:var(--text-dim);line-height:1.5;}


  .notes ul{margin:0;padding-left:18px;}

  .notes li{font-size:12px;color:var(--text-dim);line-height:1.7;}

  .notes li::marker{color:var(--accent);}
  /* ================= MY PROGRESS PANEL ================= */

  .my-progress-panel .welcome-line{
    font-size:13px;font-weight:700;color:var(--text);
    margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;gap:8px;
  }

  .my-progress-panel .welcome-line .edit-profile-link{
    font-size:11px;font-weight:600;color:var(--accent);cursor:pointer;
    background:none;border:none;font-family:inherit;padding:0;white-space:nowrap;
  }

  .my-progress-panel .welcome-line .edit-profile-link:hover{text-decoration:underline;}

  .progress-ring-row{display:flex;align-items:center;gap:16px;margin-bottom:14px;}

  .progress-ring-wrap{position:relative;width:76px;height:76px;flex:0 0 auto;}

  .progress-ring-wrap svg{width:100%;height:100%;transform:rotate(-90deg);}

  .progress-ring-wrap .ring-track{fill:none;stroke:var(--line);stroke-width:8;}

  .progress-ring-wrap .ring-fill{fill:none;stroke:var(--accent);stroke-width:8;stroke-linecap:round;transition:stroke-dashoffset .4s ease;}

  .progress-ring-wrap .ring-pct{
    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    font-size:15px;font-weight:800;color:var(--text);
  }

  .progress-ring-summary{flex:1 1 auto;min-width:0;}

  .progress-ring-summary .prs-line{font-size:12px;color:var(--text-dim);line-height:1.6;}

  .progress-ring-summary .prs-line b{color:var(--text);font-weight:700;}

  .stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;}

  .stat-box{
    background:rgba(255,255,255,.03);border:1px solid var(--line);
    border-radius:8px;padding:8px 10px;
  }

  .stat-box .sb-num{font-size:16px;font-weight:800;color:var(--text);line-height:1.3;}

  .stat-box .sb-label{font-size:10.5px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.03em;}

  .elective-subsection{
    border-top:1px dashed var(--line);padding-top:10px;margin-top:2px;
  }

  .elective-subsection .es-title{font-size:11.5px;font-weight:700;color:var(--dept-tx);background:var(--dept);display:inline-block;padding:2px 8px;border-radius:6px;margin-bottom:7px;}

  .elective-subsection .es-line{font-size:12px;color:var(--text-dim);line-height:1.6;}

  .elective-subsection .es-line b{color:var(--text);}

  .elective-subsection .es-note{font-size:11px;color:var(--text-dim);font-style:italic;margin-top:4px;line-height:1.5;}

  .elective-subsection .es-track{
    margin-top:6px;height:6px;border-radius:4px;background:var(--line);overflow:hidden;
  }

  .elective-subsection .es-fill{height:100%;background:var(--dept);border-radius:4px;transition:width .3s ease;}

  /* ==========================================================================
     9. SIDEBAR — WHAT CAN I TAKE NEXT — the unlocked-courses chip list and its
     click-to-jump highlight pulse
     ========================================================================== */
  /* ================= WHAT CAN I TAKE NEXT ================= */

  .next-courses-panel .ncp-empty{font-size:12px;color:var(--text-dim);line-height:1.6;}

  .next-courses-list{display:flex;flex-direction:column;gap:6px;}

  .next-course-chip{
    display:flex;align-items:center;justify-content:space-between;gap:8px;
    background:rgba(47,191,113,.08);border:1px solid var(--unlock);
    border-radius:8px;padding:7px 10px;cursor:pointer;
    font-size:12px;color:var(--text);text-align:left;
    font-family:inherit;transition:background .15s ease, transform .1s ease;
  }

  .next-course-chip:hover{background:rgba(47,191,113,.18);transform:translateX(2px);}

  .plan-page.rtl-mode .next-course-chip:hover{transform:translateX(-2px);}

  .next-course-chip .ncc-cr{
    flex:0 0 auto;font-size:10.5px;font-weight:700;color:var(--unlock);
    background:rgba(47,191,113,.15);padding:2px 6px;border-radius:5px;white-space:nowrap;
  }

  @keyframes jumpPulse{
    0%,100%{ box-shadow:0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
    50%{ box-shadow:0 0 0 7px color-mix(in srgb, var(--accent) 0%, transparent); }
  }

  /* js/50-whats-next.js — the ranked cards above the plain chip list. Sized
     for the card they actually mount in (the Dashboard's "What Can I Take
     Next" panel), not a full page, so they stay compact by design rather
     than being a cramped version of something bigger. */
  .wn-cards{display:flex; flex-direction:column; gap:8px; margin-bottom:8px;}

  .wn-card{
    position:relative; overflow:hidden;
    display:block; width:100%; text-align:left; font-family:inherit; cursor:pointer;
    background:var(--search-bg); border:1px solid var(--line); border-radius:10px;
    padding:10px 12px; color:var(--text);
  }
  html[dir="rtl"] .wn-card, .plan-page.rtl-mode .wn-card{text-align:right;}
  .wn-card:hover{border-color:var(--accent);}
  .wn-card.is-top{border-color:var(--accent); background:color-mix(in srgb, var(--accent) 6%, transparent);}

  /* The ghost rank number — ink-on-ink, so it reads as texture rather than
     competing with the course name for attention. */
  .wn-rank{
    position:absolute; top:2px; right:8px; font-size:34px; font-weight:800;
    color:rgba(255,255,255,.05); line-height:1; pointer-events:none;
  }
  html[dir="rtl"] .wn-rank, .plan-page.rtl-mode .wn-rank{right:auto; left:8px;}

  .wn-card-head{position:relative; display:flex; align-items:flex-start; justify-content:space-between; gap:8px;}
  .wn-card-head h4{font-size:13px; font-weight:700; margin:0; line-height:1.35;}

  .wn-tag{
    flex:0 0 auto; font-size:10px; font-weight:800; border-radius:99px;
    padding:3px 9px; white-space:nowrap;
  }
  .wn-tag-hot{background:rgba(255,182,72,.14); color:#ffb648; border:1px solid rgba(255,182,72,.4);}

  .wn-sub{position:relative; font-size:10.5px; color:var(--text-dim); margin-top:3px;}
  .wn-reason{position:relative; font-size:11.5px; color:var(--text); line-height:1.55; margin:6px 0 0;}
  .wn-reason strong{font-weight:700;}

  .wn-rest{margin-top:2px;}

  .wn-grid{display:flex; flex-direction:column; gap:10px; margin-top:10px;}

  .wn-panel{
    background:var(--search-bg); border:1px solid var(--line); border-radius:10px; padding:11px 13px;
  }
  /* A button now: these two panels sat open under the course cards and were
     most of the dashboard's text. Worth a look, not worth reading on every
     open — same tap-to-open pattern as the My Path categories and the
     Course Library shelves. */
  .wn-lbl{
    display:flex; align-items:center; gap:8px; width:100%;
    font-size:10px; font-weight:800; letter-spacing:.05em; text-transform:uppercase;
    color:var(--text-dim); margin:0; padding:2px 0;
    background:none; border:none; font-family:inherit; cursor:pointer; text-align:start;
  }
  .wn-lbl-count{
    font-size:9.5px; font-weight:800; color:var(--accent);
    background:color-mix(in srgb, var(--accent) 14%, transparent);
    border-radius:999px; padding:1px 6px;
  }
  .wn-lbl-chev{
    margin-inline-start:auto; font-size:15px; line-height:1; color:var(--accent);
    transition:transform .18s ease;
  }
  .wn-panel-open .wn-lbl-chev{transform:rotate(90deg);}
  .wn-panel-body{display:none;}
  .wn-panel-open .wn-panel-body{display:block; margin-top:10px;}
  @media (prefers-reduced-motion:reduce){ .wn-lbl-chev{transition:none;} }

  .wn-bar-row{margin-bottom:9px;}
  .wn-bar-row:last-child{margin-bottom:0;}
  .wn-bar-top{display:flex; justify-content:space-between; font-size:11px; color:var(--text-dim); margin-bottom:4px;}
  .wn-bar-top b{color:var(--text); font-weight:700;}
  .wn-bar-track{height:6px; border-radius:99px; background:rgba(255,255,255,.06); overflow:hidden;}
  .wn-bar-track i{display:block; height:100%; background:var(--accent); border-radius:99px;}

  .wn-worth-line{
    display:flex; gap:7px; font-size:11.5px; color:var(--text); line-height:1.55; margin:0 0 7px;
  }
  .wn-worth-line:last-child{margin-bottom:0;}
  .wn-worth-line:before{content:'—'; color:var(--text-dim); flex:0 0 auto;}

  /* Below 600px even one wrapped row doesn't fit two cards side by side —
     swipe between them instead, a sliver of the next one peeking in as the
     "there's more" cue. 600px, not the usual 720px breakpoint elsewhere in
     this file: between 600-720px the rule right below already lays these
     out as a wrapping row, which handles that width fine on its own. */
  @media (max-width:599px){
    .wn-cards{
      flex-direction:row; flex-wrap:nowrap;
      overflow-x:auto; scroll-snap-type:x mandatory; scrollbar-width:none;
    }
    .wn-cards::-webkit-scrollbar{display:none;}
    .wn-card{flex:0 0 85%; scroll-snap-align:start;}
  }
  @media (min-width:600px){
    .wn-cards{flex-direction:row; flex-wrap:wrap;}
    .wn-card{flex:1 1 220px; min-width:0;}
  }
  @media (min-width:720px){
    .wn-grid{flex-direction:row;}
    .wn-grid .wn-panel{flex:1 1 0; min-width:0;}
  }

  /* ==========================================================================
     10. FORMS, BUTTONS & RATING CONTROLS — shared form-field styling (used by
     the student-info dialog, new-plan dialog, etc.), plus the difficulty
     star rating and workload/status button groups used in the course modal
     ========================================================================== */
  /* ================= STUDENT INFO / FORM DIALOG ================= */

  .form-field{margin-bottom:14px;text-align:left;}

  .plan-page.rtl-mode .form-field,
  html[dir="rtl"] .form-field{text-align:right;}

  .form-field label{display:block;font-size:12px;font-weight:700;color:var(--text-dim);margin-bottom:5px;}

  .form-field input[type="text"],
  .form-field input[type="number"],
  .form-field input[type="email"],
  .form-field input[type="password"],
  .form-field select,
  .form-field textarea{
    width:100%;background:var(--search-bg);border:1px solid var(--line);
    border-radius:8px;padding:9px 11px;color:var(--text);font-size:13.5px;
    font-family:inherit;box-sizing:border-box;
  }

  .form-field textarea{resize:vertical;min-height:70px;}

  .form-field input:focus,
  .form-field select:focus,
  .form-field textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);}

  .form-radio-group{display:flex;gap:16px;}

  .form-radio-group label{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text);font-weight:500;}

  .modal-card .form-actions{display:flex;gap:10px;margin-top:18px;justify-content:flex-end;}

  .modal-card .form-note{font-size:11px;color:var(--text-dim);margin-top:10px;line-height:1.6;}

  .status-btn-group{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;}
  .who-else-note{margin:2px 0 12px;font-size:11.5px;color:var(--text-dim);}

  .status-btn{
    background:rgba(255,255,255,.03);border:1px solid var(--line);color:var(--text-dim);
    border-radius:7px;padding:6px 10px;font-size:12px;font-family:inherit;cursor:pointer;
    transition:background .15s ease,border-color .15s ease,color .15s ease;
  }

  .status-btn:hover{border-color:var(--accent);color:var(--text);}

  /* University Elective picker — a real, searchable list of that
     university's actual elective courses, opened from the course popup's
     "what did you actually take?" button instead of a free-text box. */
  .elective-picker-field .home-btn{display:flex;align-items:center;gap:8px;font-size:12.5px;text-align:left;}
  .plan-page.rtl-mode .elective-picker-field .home-btn,
  html[dir="rtl"] .elective-picker-field .home-btn{text-align:right;}
  .elective-picker-list{
    margin-top:8px;border:1px solid var(--line);border-radius:8px;
    background:var(--panel);padding:8px;
  }
  .elective-pick-options{max-height:220px;overflow-y:auto;display:flex;flex-direction:column;gap:2px;}
  .elective-pick-option{
    display:flex;align-items:baseline;gap:8px;width:100%;text-align:left;
    background:transparent;border:none;border-radius:6px;padding:7px 8px;
    color:var(--text);font-size:12.5px;font-family:inherit;cursor:pointer;
  }
  .plan-page.rtl-mode .elective-pick-option,
  html[dir="rtl"] .elective-pick-option{text-align:right;}
  .elective-pick-option:hover{background:color-mix(in srgb, var(--accent) 12%, transparent);}
  .elective-pick-option .epo-code{color:var(--text-dim);font-size:10.5px;flex:0 0 auto;}
  .elective-pick-option .epo-name{flex:1;}
  .elective-pick-clear{
    color:#ff9ecb;border-bottom:1px solid var(--line);border-radius:6px 6px 0 0;
    margin-bottom:4px;padding-bottom:9px;
  }

  .status-btn.active{background:var(--accent);border-color:var(--accent);color:var(--on-accent);font-weight:700;}
  /* Done is a real, clickable segment on every platform (js/21-course-
     modal-extras.js's bind() calls the same completion toggle the
     checkbox uses) — it used to sit disabled and dimmed here, back when
     only the checkbox could set completion. */
  .status-btn-auto.active{background:var(--unlock);border-color:var(--unlock);color:#fff;}

  .grade-select{
    background:var(--search-bg);border:1px solid var(--line);border-radius:7px;
    padding:7px 10px;color:var(--text);font-size:13px;font-family:inherit;
  }

  .grade-select:focus{outline:none;border-color:var(--accent);}

  .numeric-grade-row{display:flex;align-items:center;gap:8px;margin-top:6px;}
  .numeric-grade-input{
    background:var(--search-bg);border:1px solid var(--line);border-radius:7px;
    padding:6px 9px;color:var(--text);font-size:12.5px;font-family:inherit;width:100%;max-width:180px;
  }
  .numeric-grade-input:focus{outline:none;border-color:var(--accent);}
  .numeric-grade-result{font-size:12px;font-weight:700;color:var(--text-dim);white-space:nowrap;}
  .numeric-grade-result.ngr-active{color:var(--unlock);}

  /* ================= PASS-MARK TOGGLE + ASSESSMENT BREAKDOWN ================= */
  .pass-mark-toggle{display:flex;align-items:center;gap:6px;margin-top:8px;flex-wrap:wrap;}
  .pass-mark-toggle .pm-label{font-size:11.5px;color:var(--text-dim);}
  .pm-btn{
    background:var(--search-bg);border:1px solid var(--line);border-radius:20px;
    padding:4px 10px;color:var(--text-dim);font-size:11.5px;font-family:inherit;cursor:pointer;
    transition:background .12s ease,border-color .12s ease,color .12s ease;
  }
  .pm-btn:hover{border-color:var(--accent);}
  .pm-btn.active{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700;}

  .assessment-breakdown{margin-top:10px;padding:10px;border:1px solid var(--line);border-radius:10px;background:var(--search-bg);}
  /* Caps the row list's height instead of letting the whole course modal
     grow without bound once several assessments are added — scrolls
     internally with a slim visible thumb (like a native scrollbar, just
     thin) rather than hiding the overflow. */
  .assessment-rows{
    display:flex;flex-direction:column;gap:6px;
    max-height:190px;overflow-y:auto;padding-right:6px;
    scrollbar-width:thin;scrollbar-color:var(--line) transparent;
  }
  .assessment-rows::-webkit-scrollbar{width:5px;}
  .assessment-rows::-webkit-scrollbar-track{background:transparent;}
  .assessment-rows::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
  .assessment-rows::-webkit-scrollbar-thumb:hover{background:var(--text-dim);}
  .assessment-row{display:flex;align-items:center;gap:6px;}
  .assessment-row .ar-label{flex:1 1 auto;min-width:0;}
  .assessment-row .ar-score,.assessment-row .ar-max{width:64px;flex:0 0 64px;}
  .assessment-row input{
    background:var(--card-bg);border:1px solid var(--line);border-radius:7px;
    padding:6px 8px;color:var(--text);font-size:12.5px;font-family:inherit;
  }
  .assessment-row input:focus{outline:none;border-color:var(--accent);}
  .assessment-row .ar-sep{color:var(--text-dim);font-size:12px;}
  .ar-remove{
    background:none;border:none;color:var(--text-dim);font-size:16px;line-height:1;
    cursor:pointer;padding:2px 4px;border-radius:6px;flex:0 0 auto;
  }
  .ar-remove:hover{color:#ff5d5d;background:rgba(255,93,93,.12);}
  .assessment-empty{margin:0 0 8px;font-size:12px;}

  .assessment-quick-add{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;}
  .aq-btn{
    background:var(--card-bg);border:1px dashed var(--line);border-radius:16px;
    padding:4px 10px;color:var(--text-dim);font-size:11.5px;font-family:inherit;cursor:pointer;
  }
  .aq-btn:hover{border-color:var(--accent);color:var(--text);}

  .assessment-summary{margin-top:10px;}
  .assessment-total{margin:0 0 6px;font-size:12px;font-weight:700;color:var(--text-dim);}
  .assessment-empty-total{margin:10px 0 0;font-size:12px;}
  .assessment-need-list{
    list-style:none;margin:6px 0 0;padding:0;
    display:flex;flex-wrap:wrap;gap:5px 10px;font-size:11.5px;color:var(--text-dim);
  }
  .assessment-need-list li{white-space:nowrap;}
  .assessment-need-list li b{color:var(--text);}
  .assessment-reverse{margin:8px 0 0;font-size:11.5px;color:var(--unlock);}
  .assessment-total-row{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:8px;flex-wrap:wrap;}
  .aq-apply-btn{
    background:var(--unlock);border:none;border-radius:8px;padding:6px 12px;
    color:var(--navy-900);font-size:12px;font-weight:700;font-family:inherit;cursor:pointer;white-space:nowrap;
  }
  .aq-apply-btn:hover{filter:brightness(1.06);}

  /* ================= RATING CONTROLS (in course modal) ================= */

  .star-rating{display:flex;gap:3px;font-size:20px;line-height:1;cursor:pointer;}

  .star-rating .star{color:var(--line);transition:color .1s ease;}

  .star-rating .star.filled{color:#f2b93d;}

  .star-rating .star:hover{color:#f2b93d;}

  .workload-btn-group{display:flex;gap:6px;}

  .workload-btn.active.wb-easy{background:var(--unlock);border-color:var(--unlock);color:var(--navy-900);}

  .workload-btn.active.wb-medium{background:var(--connect);border-color:var(--connect);color:var(--navy-900);}

  .workload-btn.active.wb-hard{background:var(--prereq);border-color:var(--prereq);color:#fff;}

  .notes-textarea{
    width:100%;background:var(--search-bg);border:1px solid var(--line);
    border-radius:8px;padding:9px 11px;color:var(--text);font-size:13px;
    font-family:inherit;resize:vertical;box-sizing:border-box;
  }

  .notes-textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);}

  .notes-textarea::placeholder{color:var(--text-dim);}

  /* ==========================================================================
     11. MODALS & POPUPS — the generic modal overlay/card shell every dialog in
     this file reuses, plus the draggable floating course-info popup
     ========================================================================== */

  .plan-page.rtl-mode .modal-row{flex-direction:row-reverse;text-align:right;}
  /* ---------- course detail modal ---------- */

  .modal-overlay{
    position:fixed; inset:0;
    background:rgba(5,8,20,.65);
    display:none;
    align-items:center; justify-content:center;
    /* Must clear .app-sidebar's z-index:900 — that sidebar is fixed and
       opaque, so a lower-stacked modal renders with its left portion
       physically covered by it (looks like the dialog is cut off/missing
       its left edge whenever the sidebar is showing, i.e. whenever a plan
       is open). */
    z-index:1000;
    padding:20px;
  }

  .modal-overlay.open{display:flex;}

  .modal-card{
    background:var(--panel);
    border:1px solid var(--line);
    border-radius:12px;
    max-width:420px;
    width:100%;
    padding:24px 26px 18px;
    position:relative;
    box-shadow:0 20px 60px rgba(0,0,0,.55);
  }

  /* A drawn ✕ on the icon grid rather than the × character, which inherited
     the body font and never lined up with the icons beside it. The button
     keeps a 34px box so it stays a real tap target at 15px of glyph. */
  .modal-close{
    position:absolute; top:8px; right:10px;
    display:flex; align-items:center; justify-content:center;
    width:34px; height:34px; border-radius:50%;
    background:none; border:none;
    color:var(--text-dim);
    cursor:pointer; line-height:1; padding:0;
    transition:background .15s ease, color .15s ease;
  }

  /* One way out, not two. js/60-backbar.js puts a "← Back" bar at the top of
     every dialog on desktop, because a small ✕ in a corner is something you
     have to already know to look for. But the ✕ stayed where it was, so
     every dialog ended up with two controls doing exactly the same thing —
     and a student asked, reasonably, which one was which.
     Where a bar exists it is the way out; the ✕ steps aside. It stays in the
     DOM because the bar's handler clicks it, so whatever the dialog does on
     close still happens. Dialogs the bar deliberately skips keep their ✕,
     which is then their only control. */
  /* DESKTOP ONLY. This rule used to carry no media query, and that put an
     invisible button across the top of every dialog on every phone. The
     phone turns .modal-close into its own control further down this file
     (see the max-width:720px block); that later rule wins on width, height
     and position, but `clip-path` had no competitor, so it survived and
     clipped the phone's exit to nothing while leaving it full-width and
     clickable. Students had no visible way out of the Degree Audit,
     Achievements, Share, Contacts, Change Major, Thoughts, the Course
     Library or Plan My Next Semester — but tapping the blank strip at the
     top closed them, for no reason anyone could see. */
  @media (min-width:721px){
    .modal-card:has(> .back-bar) > .modal-close,
    .modal-card:has(.back-bar) > .modal-close{
      position:absolute; width:1px; height:1px; padding:0;
      margin:-1px; overflow:hidden; clip-path:inset(50%);
      white-space:nowrap; border:0;
    }
  }

  .modal-close:hover{color:var(--text); background:var(--search-bg);}
  .modal-close:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  .modal-body h3{margin:0 6px 4px 0;font-size:16px;font-weight:800;color:var(--text);line-height:1.3;}

  .modal-num{font-size:11.5px;color:var(--accent);font-weight:700;letter-spacing:.05em;margin-bottom:14px;}

  .modal-row{
    display:flex; justify-content:space-between; gap:16px;
    padding:9px 0;
    border-top:1px solid var(--line);
    font-size:12.5px;
  }

  .modal-row .k{color:var(--text-dim);font-weight:600;flex:0 0 auto;}

  .modal-row .v{color:var(--text);font-weight:700;text-align:right;}
  /* ================= floating draggable popup ================= */

  .float-popup{
    position:fixed;
    top:110px; right:26px;
    width:300px;
    max-width:calc(100vw - 24px);
    background:var(--panel);
    border:1px solid var(--accent);
    border-radius:12px;
    box-shadow:0 20px 55px rgba(0,0,0,.55);
    z-index:1000; /* same app-sidebar-clearance reason as .modal-overlay above */
    overflow:hidden;
    animation:popupIn .18s ease-out;
  }

  @keyframes popupIn{
    from{opacity:0; transform:translateY(-8px) scale(.97);}
    to{opacity:1; transform:translateY(0) scale(1);}
  }

  .float-popup-head{
    display:flex;align-items:center;justify-content:space-between;
    gap:10px;
    padding:11px 8px 11px 16px;
    background:color-mix(in srgb, var(--accent) 10%, transparent);
    border-bottom:1px solid var(--line);
    cursor:grab;
    user-select:none;
    /* Without this, a touch-drag on mobile races the browser's own "is this
       a page scroll?" gesture detection — it sometimes wins and cancels the
       pointer sequence mid-drag, making the popup feel stuck or jumpy. */
    touch-action:none;
  }

  .float-popup-head:active{cursor:grabbing;}

  .float-popup-title{font-size:13px;font-weight:800;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

  .float-popup-head-actions{display:flex;align-items:center;gap:2px;flex:0 0 auto;}

  .float-popup-min, .float-popup-close{
    background:none;border:none;color:var(--text-dim);
    font-size:13px;line-height:1;cursor:pointer;padding:6px 8px;border-radius:6px;
    display:flex;align-items:center;justify-content:center;
  }

  .float-popup-close{font-size:20px;}

  .float-popup-min:hover, .float-popup-close:hover{color:#fff;background:rgba(255,255,255,.08);}

  .float-popup-min .fp-min-arrow{display:inline-block;transition:transform .2s ease;}

  /* Collapsed to just the head bar — lets the page underneath be read or
     tapped without fully closing (and losing) the popup's own state. The
     arrow flips to point the opposite way it did before the tap, same
     affordance as the legend's own expand/collapse arrow. */
  .float-popup.minimized .float-popup-min .fp-min-arrow{transform:rotate(180deg);}
  .float-popup.minimized .float-popup-body{display:none;}

  .float-popup-body{
    padding:14px 16px 16px;
    max-height:60vh;
    overflow-y:auto;
    font-size:12.5px;
    color:var(--text-dim);
    line-height:1.6;
  }

  .float-popup-body h4{
    margin:14px 0 6px;font-size:11.5px;font-weight:800;
    color:var(--text);text-transform:uppercase;letter-spacing:.04em;
  }

  .float-popup-body h4:first-child{margin-top:0;}

  .float-popup-body h4 .badge-ic{margin-right:5px;}

  .float-popup-body ul{margin:0;padding-left:18px;}

  .float-popup-body li{margin:3px 0;color:var(--text);}

  .float-popup-body .fp-empty{color:var(--text-dim);font-style:italic;}

  .float-popup-body .fp-why{color:var(--text-dim);font-size:12px;}

  .float-popup-body .fp-warn{
    color:var(--text);
    font-size:12px;
    background:rgba(224,50,47,.1);
    border-left:3px solid var(--free);
    border-radius:6px;
    padding:9px 10px;
    line-height:1.6;
  }

  .float-popup[dir="rtl"] .fp-warn{border-left:none;border-right:3px solid var(--free);}

  .float-popup[dir="rtl"] .float-popup-body ul{padding-left:0;padding-right:18px;}

  .float-popup[dir="rtl"] .float-popup-head{cursor:grab;}

  /* Placed AFTER the base .float-popup rule above on purpose — same
     specificity, so source order decides, and a mobile override declared
     BEFORE the rule it's meant to override silently loses regardless of
     the media query (this bit the course grid earlier; same fix here). */
  @media (max-width:640px){
    .float-popup{left:12px;right:12px;width:auto;top:auto;bottom:14px;}
    /* 60vh (the desktop default) combined with a near-full-width popup
       reads as "covering the whole screen" on a phone — capped lower so
       the page underneath stays visibly reachable even before minimizing. */
    .float-popup-body{max-height:42vh;}
  }
  /* ================= COURSE MODAL: STATUS + GRADE EXTRAS ================= */

  .modal-extras{border-top:1px solid var(--line);margin-top:14px;padding-top:14px;}

  .modal-extras .modal-row-block{margin-bottom:12px;}

  .modal-extras .k{font-size:11.5px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:.03em;display:block;margin-bottom:7px;}

  .modal-extras .ex-note{font-size:11.5px;color:var(--text-dim);font-style:italic;line-height:1.6;}

  /* ==========================================================================
     12. DEGREE AUDIT & GPA — the requirement-by-category table and the GPA/
     academic-standing dashboard cards
     ========================================================================== */
  /* ================= GPA / DEGREE AUDIT MODAL ================= */

  /* Also the shared card for Achievements and Advisor (see index.html), so
     the width bump for the GPA table below is scoped to THIS modal only —
     #auditModalOverlay's own .modal-card, not the class everywhere it's
     used — rather than widening those other two along with it. */
  .audit-modal-card{max-width:720px;width:92vw;max-height:85vh;overflow-y:auto;overflow-x:hidden;}
  #auditModalOverlay .modal-card{max-width:960px;}

  .gpa-dashboard{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:16px 0 20px;}

  @media (max-width:640px){ .gpa-dashboard{grid-template-columns:1fr;} }

  .gpa-card{background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:10px;padding:12px 14px;text-align:center;}

  .gpa-card .gc-num{font-size:22px;font-weight:800;color:var(--text);line-height:1.3;}

  .gpa-card .gc-label{font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.03em;margin-top:2px;}

  .standing-badge{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12.5px;font-weight:700;margin-top:4px;}

  .standing-good{background:rgba(47,191,113,.18);color:var(--unlock);}

  .standing-warning{background:rgba(224,168,63,.18);color:var(--connect);}

  .standing-probation{background:rgba(224,83,140,.18);color:var(--prereq);}

  .standing-none{background:rgba(255,255,255,.06);color:var(--text-dim);}

  /* js/51-gpa-studio.js — the editable grade table, the GPA dial and the
     target-GPA projection inserted into the Degree Audit modal in place of
     the old flat three-card row. Written mobile-first: the table and the
     side column stack in document order (table first, since that is what a
     screen reader or a narrow phone should meet before the summary of it),
     and only become side by side once there is room. The table scrolls
     inside its own wrapper on a narrow screen rather than the columns
     being squeezed, so a long course name and its grade dropdown both
     stay legible on a phone. */
  .gs-layout{display:flex; flex-direction:column; gap:18px; margin:16px 0 20px;}
  .gs-col-main{min-width:0;}
  .gs-col-side{display:flex; flex-direction:column; gap:14px; min-width:0;}

  /* 860px, not the usual 720px this codebase uses elsewhere: this layout
     lives inside .audit-modal-card, which is capped at 92vw — so a 720px
     viewport only ever hands it about 660px, less than the table's own
     min-width plus a usable side column need. 860px was picked backwards
     from that cap (0.92 x 860 =~ 790px, comfortably more than the two
     columns' combined minimums) rather than copied from elsewhere, since a
     media query answers "how wide is the viewport", not "how wide is this
     modal" — the two are not the same question the moment a card has its
     own max-width, and treating them as one is what actually overflowed
     here the first time. */
  @media (min-width:860px){
    .gs-layout{flex-direction:row; align-items:flex-start;}
    .gs-col-main{flex:1.4 1 0;}
    .gs-col-side{flex:1 1 0;}
  }

  /* Below the phone breakpoint, swipe between the two instead of stacking —
     scroll-snap does the actual paging, js/51-gpa-studio.js's bindSwipeDots
     only keeps the two dots in sync with which page is showing. */
  .gs-swipe-dots{display:none;}
  @media (max-width:720px){
    .gs-swipe-dots{display:flex; justify-content:center; gap:6px; margin:0 0 8px;}
    .gs-swipe-dots span{width:6px; height:6px; border-radius:50%; background:var(--line);}
    .gs-swipe-dots span.active{background:var(--accent);}
    .gs-layout{
      flex-direction:row; flex-wrap:nowrap;
      overflow-x:auto; scroll-snap-type:x mandatory; scrollbar-width:none;
    }
    .gs-layout::-webkit-scrollbar{display:none;}
    .gs-col-main, .gs-col-side{flex:0 0 100%; min-width:100%; scroll-snap-align:start;}
  }

  .gs-card{
    background:rgba(255,255,255,.03); border:1px solid var(--line); border-radius:12px;
    padding:16px 18px;
  }

  .gs-dial{
    width:132px; height:132px; border-radius:50%; margin:6px auto 14px;
    display:flex; align-items:center; justify-content:center;
  }
  .gs-dial-inner{
    width:104px; height:104px; border-radius:50%; background:var(--panel);
    display:flex; flex-direction:column; align-items:center; justify-content:center;
  }
  .gs-dial-inner b{font-size:28px; font-weight:800; color:var(--text); line-height:1.15;}
  .gs-dial-inner span{font-size:9.5px; color:var(--text-dim); letter-spacing:.06em; margin-top:2px;}

  .gs-kv{
    display:flex; justify-content:space-between; align-items:center; gap:10px;
    padding:8px 0; border-bottom:1px solid var(--line); font-size:12.5px; color:var(--text-dim);
  }
  .gs-kv:last-child{border-bottom:0;}
  .gs-kv b{color:var(--text); font-weight:700;}

  .gs-block{margin:0;}

  .gs-lbl{font-size:11px;font-weight:800;letter-spacing:.03em;text-transform:uppercase;color:var(--text-dim);margin-bottom:4px;}

  .gs-hint{font-size:11.5px;color:var(--text-dim);margin:0 0 10px;line-height:1.55;}

/* The way out of an empty Degree Audit. Grades are entered from a course,
   not from this screen, so with none entered it explained where to go and
   gave no way to go there. */
.gs-goplan{
  /* The button is the whole point of this empty state, and it was tucked
     against the left edge of the block hugging its own label. Centred in the
     block, with the icon and the label centred inside it. */
  display:flex; align-items:center; justify-content:center; gap:9px;
  width:fit-content; max-width:100%; min-width:min(250px, 100%);
  margin:14px auto 0; text-align:center;
  padding:13px 22px; border-radius:999px;
  background:var(--accent); color:var(--on-accent);
  border:none; font-family:inherit; font-size:14.5px; font-weight:800;
  cursor:pointer; transition:filter .15s ease;
}
.gs-goplan:hover{filter:brightness(1.08);}
.gs-goplan:focus-visible{outline:2px solid var(--accent); outline-offset:3px;}

  .gs-empty{font-size:12.5px;color:var(--text-dim);}

  /* The transcript-shaped record under the Degree Audit (js/19-audit.js).
     Three columns read down, so the numbers line up: tabular figures, and a
     cumulative row that reads as a total rather than another term. */
  .tr-wrap{overflow-x:auto; margin-bottom:12px;}
  .tr-table{width:100%; border-collapse:collapse; font-size:12.5px;}
  .tr-table th{
    text-align:start; color:var(--text-dim); font-weight:700; font-size:10.5px;
    text-transform:uppercase; letter-spacing:.03em; padding:8px 6px;
    border-bottom:1px solid var(--line);
  }
  .tr-table td{padding:8px 6px; border-bottom:1px solid var(--line);}
  .tr-table .tr-num{text-align:end; font-variant-numeric:tabular-nums; white-space:nowrap;}
  .tr-table tfoot td{
    border-bottom:none; border-top:2px solid var(--line);
    font-weight:800; color:var(--text);
  }
  .tr-why{margin-bottom:16px;}
  .tr-why summary{
    cursor:pointer; font-size:11.5px; font-weight:700; color:var(--text-dim);
    list-style:revert;
  }
  .tr-why summary:hover{color:var(--text);}
  .tr-why p{margin:6px 0 0; font-size:11.5px; line-height:1.6; color:var(--text-dim);}

  /* Idea 58 — your load against the advisory plan (js/19-audit.js). Two bars
     per term: what the plan schedules there, and what you have finished of
     it. The plan bar is the outline you are measured against, so it is a
     hollow accent-edged column; the finished bar is solid and sits in front
     of it, which is the same figure/ground reading as a target and a
     result. The chart scrolls sideways rather than squeezing: eleven terms
     at 26px still overflow a 390px phone, and squeezed bars stop being
     comparable to each other, which is the entire point of the widget. */
  .adv-block{margin-bottom:16px;}
  .adv-chart{
    display:flex; align-items:flex-end; gap:10px;
    height:112px; padding:0 2px 0; overflow-x:auto; overflow-y:hidden;
  }
  .adv-term{
    flex:0 0 auto; display:flex; flex-direction:column; align-items:center;
    gap:5px; height:100%;
  }
  .adv-pair{
    position:relative; flex:1 1 auto; width:26px;
    display:flex; align-items:flex-end; justify-content:center;
  }
  .adv-plan{
    width:100%; min-height:2px; border-radius:4px 4px 0 0;
    border:1.5px solid var(--accent); border-bottom:none;
    background:color-mix(in srgb, var(--accent) 12%, transparent);
  }
  /* In front of the plan outline, not beside it — a term is one quantity
     measured against another, and two adjacent columns read as two terms. */
  .adv-mine{
    position:absolute; inset-inline:5px; bottom:0;
    min-height:2px; border-radius:3px 3px 0 0; background:var(--accent);
  }
  .adv-lab{
    font-size:10px; font-weight:700; color:var(--text-dim);
    font-variant-numeric:tabular-nums; white-space:nowrap;
  }
  .adv-key{
    display:flex; align-items:center; gap:6px; flex-wrap:wrap;
    margin:0 0 8px; font-size:11px; color:var(--text-dim);
  }
  .adv-swatch{
    width:11px; height:11px; border-radius:3px; flex:0 0 auto;
    margin-inline-start:4px;
  }
  .adv-key .adv-swatch:first-child{margin-inline-start:0;}
  .adv-swatch-mine{background:var(--accent);}
  .adv-swatch-plan{
    border:1.5px solid var(--accent);
    background:color-mix(in srgb, var(--accent) 12%, transparent);
  }

  /* The three automatic restore points in Settings (js/84-autobackup.js).
     A date, how long ago, how big, and the two things you can do with it. */
  .ab-list{display:flex; flex-direction:column; gap:8px; margin-bottom:6px;}
  .ab-row{
    display:flex; align-items:center; justify-content:space-between;
    gap:10px; flex-wrap:wrap;
    padding:10px 12px; border:1px solid var(--line); border-radius:10px;
    background:var(--panel);
  }
  .ab-when{display:flex; flex-direction:column; gap:2px; min-width:0;}
  .ab-when b{font-size:13px; font-variant-numeric:tabular-nums;}
  .ab-when span{font-size:11px; color:var(--text-dim);}
  .ab-acts{display:flex; gap:6px; flex-wrap:wrap;}


  /* ONE COURSE OPEN AT A TIME (js/51-gpa-studio.js).
     This was a four-column table with every course's fourteen grade chips
     permanently expanded — correct per row, unusable as a list: forty courses
     came to about eight thousand pixels of scrolling. A row is a summary now,
     and only the row being edited shows its chips. */
  .gs-list{
    border:1px solid var(--line); border-radius:10px; overflow:hidden;
  }
  .gs-row + .gs-row{border-top:1px solid var(--line);}
  .gs-row.is-open{background:color-mix(in srgb, var(--accent) 6%, transparent);}

  .gs-head{
    display:flex; align-items:center; gap:12px; width:100%;
    padding:11px 13px; text-align:start;
    background:none; border:none; color:var(--text); font-family:inherit;
    cursor:pointer;
  }
  .gs-head:hover{background:color-mix(in srgb, var(--accent) 5%, transparent);}
  .gs-head:focus-visible{outline:2px solid var(--accent); outline-offset:-2px;}
  .gs-head-main{display:flex; flex-direction:column; gap:2px; flex:1; min-width:0;}
  .gs-head-name{
    font-size:13px; font-weight:700; line-height:1.35;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .gs-head-right{display:flex; align-items:center; gap:9px; flex:none;}

  /* The grade stays readable while the row is closed, so the whole record can
     be scanned without opening anything. */
  .gs-head-grade{
    min-width:30px; text-align:center;
    font-size:12.5px; font-weight:800; padding:4px 9px; border-radius:7px;
    background:var(--accent); color:var(--on-accent);
  }
  .gs-head-grade.is-fail{background:var(--free); color:var(--free-tx);}
  .gs-head-set{
    font-size:11px; font-weight:700; color:var(--accent);
    border:1px dashed var(--accent); border-radius:7px; padding:4px 10px;
    white-space:nowrap;
  }
  .gs-head-pts{
    min-width:38px; text-align:end; font-size:11.5px; font-weight:700;
    color:var(--text-dim); font-variant-numeric:tabular-nums;
  }
  /* What was touched this sitting — a run of edits can be checked afterwards
     rather than remembered. */
  .gs-head-changed{
    font-size:9px; font-weight:800; letter-spacing:.05em; text-transform:uppercase;
    color:var(--skills); border:1px solid var(--skills); border-radius:5px;
    padding:2px 6px; white-space:nowrap;
  }

  .gs-panel{padding:0 13px 13px;}
  .gs-panel .gs-grade-chip{min-width:38px; padding:9px 8px; font-size:12px;}

  .gs-code{color:var(--text-dim); font-size:10.5px;}
  .gs-excluded{color:var(--text-dim); font-size:11px; font-style:italic;}
  .gs-row-excluded{opacity:.6;}

  .gs-grade-select{
    background:var(--search-bg); border:1px solid var(--line); border-radius:7px;
    padding:5px 3px; color:var(--text); font-size:11.5px; font-family:inherit; width:100%;
  }
  .gs-grade-select:focus{outline:none; border-color:var(--accent);}

  .gs-project-row{display:flex; gap:14px; flex-wrap:wrap; align-items:flex-end; margin-bottom:10px;}
  .gs-remaining{font-size:12px; color:var(--text-dim); display:flex; flex-direction:column; gap:3px;}
  .gs-remaining b{font-size:18px; color:var(--text); font-weight:800;}
  .gs-project-result{
    font-size:12.5px; color:var(--text); line-height:1.6; min-height:1.6em;
    padding:10px 12px; border-radius:9px;
    background:color-mix(in srgb, var(--accent) 6%, transparent); border:1px solid var(--line);
  }
  .gs-project-result:empty{display:none;}

  .audit-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}

  /* Phone-only glanceable summary above the table (js/19-audit.js's
     auditRingsHtml) — purely additive, the table below still carries
     every column. */
  .audit-rings{display:none;}
  @media (max-width:720px){
    .audit-rings{
      display:flex; gap:10px; overflow-x:auto; -webkit-overflow-scrolling:touch;
      margin:2px 0 14px; padding-bottom:2px;
    }
    .audit-ring-card{
      flex:none; width:88px; background:var(--panel); border-radius:16px;
      padding:12px 8px; text-align:center; box-shadow:0 4px 14px rgba(0,0,0,.22);
    }
    .audit-mini-ring{width:44px; height:44px; margin:0 auto 8px; transform:rotate(-90deg);}
    .audit-mini-ring .track{fill:none; stroke:var(--header-bg); stroke-width:5;}
    .audit-mini-ring .val{fill:none; stroke-width:5; stroke-linecap:round;}
    .ar-lbl{font-size:10px; font-weight:700; color:var(--text-dim); line-height:1.25; height:25px; overflow:hidden;}
    .ar-frac{font-size:10px; color:var(--text-faint); font-weight:700; margin-top:2px;}
  }

  .audit-table{width:100%;border-collapse:collapse;font-size:12.5px;margin-top:6px;}

  @media (max-width:720px){
    /* table-layout:auto (the default) sizes each column to its own content —
       for 6 columns that means "Specialization Req." and "Planned Next" both
       fighting for room, and the losing columns (Missing, most often) get
       pushed off the edge of the .audit-table-wrap scroll area entirely
       instead of just wrapping tighter. Fixed, explicit shares make every
       column actually fit the one width that's available, at the cost of
       wrapping category names and headers onto more lines. */
    /* Six columns squeezed into a phone's width with break-word turned the
       headers into "REQUI RED" and "MISSI NG". .audit-table-wrap already
       scrolls sideways, so the table is allowed to be as wide as its own
       words need and the reader swipes — which is what that wrapper is for. */
    .audit-table{table-layout:auto; width:auto; min-width:100%; font-size:10.5px;}
    .audit-table th, .audit-table td{white-space:nowrap;}
    .audit-table th:first-child, .audit-table td:first-child{
      white-space:normal; min-width:96px; position:sticky; inset-inline-start:0;
      background:var(--panel); z-index:1;
    }
    .audit-table th{font-size:8.5px; padding:5px 7px; line-height:1.25;}
    .audit-table td{padding:5px 7px;}
    .audit-table td.cat-name{gap:4px;}
  }

  .audit-table th{
    text-align:left;color:var(--text-dim);font-weight:700;font-size:10.5px;
    text-transform:uppercase;letter-spacing:.03em;padding:6px 8px;border-bottom:1px solid var(--line);
  }

  .plan-page.rtl-mode ~ .audit-table th, .audit-table.rtl th{text-align:right;}

  .audit-table td{padding:7px 8px;border-bottom:1px solid var(--line);color:var(--text);}

  .audit-table td.zero{color:var(--text-dim);}

  .audit-table tr:last-child td{border-bottom:none;}

  .audit-table td.cat-name{font-weight:700;display:flex;align-items:center;gap:7px;}

  .audit-cat-dot{width:10px;height:10px;border-radius:3px;flex:0 0 auto;display:inline-block;}

  .audit-table .col-completed{color:var(--unlock);}

  .audit-table .col-inprogress{color:var(--connect);}

  .audit-table .col-planned{color:var(--accent);}

  .audit-table .col-missing{color:var(--prereq);}

  /* ================= ROADMAP ("Your path through <major>") ================= */
  .ro-head{display:flex; align-items:flex-start; justify-content:space-between; gap:14px; flex-wrap:wrap; margin-bottom:6px;}
  .ro-uni{font-size:11px; font-weight:700; color:var(--text-dim); white-space:nowrap; padding-top:4px;}
  .ro-empty{font-size:12.5px; color:var(--text-dim);}

  .ro-timeline{
    display:flex; align-items:flex-start; margin:22px 0 18px;
    overflow-x:auto; -webkit-overflow-scrolling:touch;
  }
  .ro-node-wrap{
    display:flex; flex-direction:column; align-items:center; gap:4px; flex:none;
    width:92px; text-align:center;
    /* It is a <button> now: pressing one scopes the breakdown below to that
       year (or the whole plan). Reset the UA chrome so it still reads as part
       of the track. */
    background:none; border:0; padding:2px 0; margin:0; cursor:pointer;
    font:inherit; color:inherit; border-radius:10px;
  }
  .ro-node{width:16px; height:16px; border-radius:50%; margin-top:2px; box-shadow:0 0 0 3px var(--panel);}
  .ro-node-done{background:#2ecc71;}
  .ro-node-current{background:var(--accent); box-shadow:0 0 0 3px var(--panel), 0 0 0 5px color-mix(in srgb, var(--accent) 35%, transparent);}
  .ro-node-future{background:var(--line);}
  .ro-node-grad{border-radius:4px; transform:rotate(45deg);}
  .ro-node-label{font-size:12px; font-weight:800; color:var(--text); margin-top:2px;}
  .ro-node-sub{font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.03em;}
  .ro-connector{flex:1; height:2px; min-width:16px; margin-top:10px; background:var(--line);}
  .ro-connector-done{background:#2ecc71;}
  .ro-connector-current{background:linear-gradient(90deg,#2ecc71,var(--accent));}

  .ro-cards{display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:10px; margin-bottom:20px;}
  .ro-card{background:var(--panel); border:1px solid var(--line); border-radius:11px; padding:12px;}
  .ro-card-current{border-color:var(--accent);}
  .ro-card-done{opacity:.72;}
  .ro-course{display:flex; align-items:baseline; gap:6px; padding:6px 0; border-top:1px solid var(--line); font-size:12px;}
  .ro-course:first-child{border-top:none; padding-top:0;}
  .ro-course-name{flex:1 1 auto; min-width:0; color:var(--text); overflow-wrap:anywhere;}
  .ro-course-cr{color:var(--text-dim); font-size:10.5px; white-space:nowrap; flex:none;}
  /* Reserved on every row (not just done ones) so the name column still
     lines up whether or not a course carries the mark — a check that only
     sometimes exists would shift every other row's text sideways next to it. */
  .ro-course-check{
    flex:none; align-self:center; width:16px; height:16px; border-radius:50%;
    display:flex; align-items:center; justify-content:center; visibility:hidden;
    background:color-mix(in srgb, var(--accent) 16%, transparent); border:1.5px solid var(--accent);
  }
  .ro-course-check .app-icon-svg{width:9px; height:9px; color:var(--accent);}
  .ro-course-done .ro-course-check{visibility:visible;}
  /* Was a plain div in the accent colour — it read as a link and did
     nothing when pressed. Now a real button that opens the rest of the year
     in place, and it looks pressable. */
  /* A 1px border in the plan's own accent colour read as plain coloured
     text at a glance — which is exactly what a student screenshotted and
     asked "shouldn't this be pressable?" about, even though the click
     handler underneath it already worked. A filled pill with a chevron
     that flips is the same control read as a button at any zoom level. */
  .ro-more{
    display:inline-flex;align-items:center;gap:6px;margin-top:8px;
    font-size:11px;font-weight:800;font-family:inherit;cursor:pointer;
    color:var(--on-accent);background:var(--accent);
    border:1px solid var(--accent);border-radius:999px;padding:5px 12px 5px 10px;
    transition:filter .12s ease,transform .12s ease;
  }
  .ro-more:hover{filter:brightness(1.08);}
  .ro-more:active{transform:scale(.97);}
  .ro-more:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .ro-more::before{
    content:'';width:7px;height:7px;flex:0 0 auto;
    border-inline-end:2px solid currentcolor;border-block-end:2px solid currentcolor;
    transform:rotate(45deg);margin-block-start:-3px;
    transition:transform .15s ease;
  }
  .ro-card-open .ro-more::before{transform:rotate(-135deg);margin-block-start:2px;}
  .ro-course-extra{display:none;}
  .ro-card-open .ro-course-extra{display:flex;}
  .ro-more .ro-more-close{display:none;}
  .ro-card-open .ro-more .ro-more-open{display:none;}
  .ro-card-open .ro-more .ro-more-close{display:inline;}

  /* Said out loud when the recorded course list does not add up to the
     university's published degree hours. */
  .ro-total-note{
    font-size:11.5px;line-height:1.5;color:var(--text-dim);
    border-left:2px solid var(--connect);padding:6px 10px;margin:10px 0 0;
    background:rgba(224,168,63,.08);border-radius:0 6px 6px 0;
  }
  .ro-card-grad{background:color-mix(in srgb, var(--accent) 6%, transparent); border-color:var(--accent);}

  /* Below the phone breakpoint the auto-fit grid above already collapses to
     two cramped columns — same swipe-between-slides treatment as the GPA
     Studio panels and the dashboard's own stat tiles. */
  .ro-swipe-dots{display:none;}
  @media (max-width:720px){
    .ro-swipe-dots{display:flex; justify-content:center; gap:6px; margin:0 0 8px;}
    .ro-swipe-dots span{width:6px; height:6px; border-radius:50%; background:var(--line);}
    .ro-swipe-dots span.active{background:var(--accent);}
    .ro-cards{
      display:flex; flex-direction:row; flex-wrap:nowrap;
      overflow-x:auto; scroll-snap-type:x mandatory; scrollbar-width:none;
    }
    .ro-cards::-webkit-scrollbar{display:none;}
    .ro-cards > .ro-card{flex:0 0 100%; min-width:100%; scroll-snap-align:start;}
  }

  .ro-summary{
    display:flex; align-items:center; gap:16px; flex-wrap:wrap;
    background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px 18px;
  }
  .ro-summary-pct, .ro-summary-stat{display:flex; flex-direction:column; gap:1px; flex:none;}
  .ro-summary-pct b, .ro-summary-stat b{font-size:19px; font-weight:800; color:var(--text);}
  .ro-summary-pct span, .ro-summary-stat span{font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.03em;}
  .ro-summary-bar{flex:1 1 100px; min-width:100px; height:8px; border-radius:99px; background:rgba(255,255,255,.08); overflow:hidden;}
  .ro-summary-bar span{display:block; height:100%; background:linear-gradient(90deg,#2ecc71,var(--accent));}

  .ro-node-wrap:hover .ro-node-label{color:var(--accent);}
  .ro-node-wrap:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
  .ro-node-active .ro-node-label{color:var(--accent);}
  .ro-node-active .ro-node{box-shadow:0 0 0 3px var(--panel), 0 0 0 6px color-mix(in srgb, var(--accent) 45%, transparent);}

  /* Requirement-category breakdown, mirroring the shape a student already
     knows from the university's own portal. */
  .ro-breakdown{
    background:var(--panel); border:1px solid var(--line);
    border-radius:14px; padding:16px 18px; margin:4px 0 18px;
  }
  .ro-bd-head h3{margin:0; font-size:15px;}
  .ro-bd-summary{
    display:flex; align-items:center; justify-content:space-between;
    gap:16px; flex-wrap:wrap; margin-top:14px;
  }
  .ro-bd-figures{display:flex; gap:22px; flex-wrap:wrap;}
  .ro-bd-fig{display:flex; flex-direction:column; gap:2px; min-width:0;}
  .ro-bd-fig b{font-size:20px; font-weight:800; color:var(--text);}
  .ro-bd-fig-label{font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.03em;}
  .ro-bd-ring{
    flex:none; width:78px; height:78px; border-radius:50%;
    display:grid; place-items:center;
    background:conic-gradient(var(--accent) calc(var(--ro-pct) * 1%), var(--line) 0);
  }
  .ro-bd-ring span{
    width:60px; height:60px; border-radius:50%; background:var(--panel);
    display:grid; place-items:center; font-size:16px; font-weight:800; color:var(--text);
  }
  .ro-bd-totals{display:flex; gap:10px; flex-wrap:wrap; margin-top:14px;}
  .ro-bd-total{
    flex:1 1 140px; min-width:0; border-radius:12px; padding:10px 14px;
    display:flex; flex-direction:column; gap:1px;
  }
  .ro-bd-total b{font-size:19px; font-weight:800; color:var(--text);}
  .ro-bd-total span{font-size:11px; color:var(--text-dim);}
  .ro-bd-total-earned{background:rgba(46,204,113,.12);}
  .ro-bd-total-left{background:color-mix(in srgb, var(--accent) 10%, transparent);}

  .ro-bd-cats{display:flex; flex-direction:column; gap:10px; margin-top:14px;}
  .ro-bd-cat{border:1px solid var(--line); border-radius:12px; padding:12px 14px;}
  .ro-bd-cat-head{display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;}

/* A category card opens to show the courses it is made of. It reported four
   numbers and nothing else, and the question a student has reading
   "Colg. Req. — 59 hours, 0 earned" is which courses those are. */
.ro-bd-cat-expandable{cursor:pointer;}
.ro-bd-cat-expandable:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
.ro-bd-cat-more{
  display:flex; align-items:center; gap:6px; margin-top:10px;
  font-size:11px; font-weight:800; letter-spacing:.04em; text-transform:uppercase;
  color:var(--accent);
}
.ro-bd-chev{display:inline-block; transition:transform .18s ease; font-size:15px; line-height:1;}
.ro-bd-cat-open .ro-bd-chev{transform:rotate(90deg);}
.ro-bd-courses{
  display:none; list-style:none; margin:10px 0 0; padding:10px 0 0;
  border-top:1px solid var(--line);
}
.ro-bd-cat-open .ro-bd-courses{display:block;}
.ro-bd-course{
  display:flex; align-items:baseline; gap:8px; padding:6px 0;
  font-size:12.5px; line-height:1.4;
}
.ro-bd-course + .ro-bd-course{border-top:1px solid color-mix(in srgb, var(--line) 55%, transparent);}
.ro-bd-course-name{flex:1; min-width:0; overflow-wrap:anywhere;}
.ro-bd-course-num{
  font-size:10.5px; color:var(--text-dim); font-variant-numeric:tabular-nums; flex:none;
}
.ro-bd-course-cr{
  font-size:11px; font-weight:800; color:var(--text-dim);
  font-variant-numeric:tabular-nums; flex:none;
}
.ro-bd-course.is-done .ro-bd-course-name{color:var(--unlock);}
.ro-bd-course.is-done .ro-bd-course-cr{color:var(--unlock);}
.ro-bd-course.is-open .ro-bd-course-cr{color:var(--accent);}
@media (prefers-reduced-motion:reduce){ .ro-bd-chev{transition:none;} }

  .ro-bd-cat-name{font-size:13.5px; font-weight:800; color:var(--text);}
  .ro-bd-cat-tag{font-size:11px; font-weight:700; color:var(--text-dim);}
  .ro-bd-cat-tag-opt{color:var(--accent);}
  .ro-bd-bar{
    height:8px; border-radius:99px; background:rgba(255,255,255,.08);
    overflow:hidden; margin:9px 0 10px;
  }
  .ro-bd-bar span{display:block; height:100%; background:linear-gradient(90deg,#2ecc71,var(--accent));}
  .ro-bd-stats{display:flex; gap:10px; flex-wrap:wrap;}
  .ro-bd-stats > div{flex:1 1 66px; min-width:0; display:flex; flex-direction:column; gap:1px;}
  .ro-bd-stats b{font-size:15px; font-weight:800; color:var(--text);}
  .ro-bd-stats span{font-size:10px; color:var(--text-dim);}

  @media (max-width:520px){
    .ro-bd-figures{gap:14px;}
    .ro-bd-ring{width:64px; height:64px;}
    .ro-bd-ring span{width:48px; height:48px; font-size:14px;}
  }

  html[dir="rtl"] .ro-course, .plan-page.rtl-mode .ro-course{direction:rtl;}

  .ro-plan-label{margin:0; font-size:11.5px; font-weight:700; color:var(--text-dim);}

  /* My Path's own reference mockup gives this screen a shape none of the
     app's other tracks use: a connected VERTICAL timeline running top to
     bottom, one row per year, instead of a horizontal dot-strip you scroll
     sideways through. Phone-only structural redesign (same convention as
     the rest of this pass) — the underlying markup, the data-ro-scope
     click-to-breakdown behaviour, and the category breakdown panel below
     are all untouched; only how the existing timeline nodes are laid out
     changes here. */
  @media (max-width:720px){
    .ro-head{flex-direction:column; gap:4px;}
    .ro-uni{padding-top:0;}

    .ro-timeline{
      flex-direction:column; align-items:stretch; overflow-x:visible; margin:18px 0;
    }
    .ro-node-wrap{
      flex-direction:row; align-items:flex-start; justify-content:flex-start;
      width:100%; text-align:left; gap:12px; padding:0;
    }
    [dir="rtl"] .ro-node-wrap, .plan-page.rtl-mode .ro-node-wrap{text-align:right;}
    .ro-node{margin-top:3px; flex:none;}
    .ro-node-label{margin-top:0; font-size:13.5px;}
    .ro-node-sub{text-transform:none; letter-spacing:0; font-size:11px;}
    /* "you are here" / "done" carries real meaning — plain lowercase grey
       hours text is what future years get, so only the two informative
       states earn a colour. */
    .ro-node-wrap:has(.ro-node-current) .ro-node-sub{color:var(--accent); font-weight:700;}
    .ro-node-wrap:has(.ro-node-done) .ro-node-sub{color:#2ecc71;}
    /* The connector was a horizontal bar between side-by-side nodes; here it
       is a short vertical bar under each dot, indented to its centre (the
       dot is 16px wide) so it reads as one continuous line down the page. */
    .ro-connector{
      width:2px; height:20px; min-width:0; flex:none;
      margin:0 0 0 7px; background:var(--line);
    }
    [dir="rtl"] .ro-connector, .plan-page.rtl-mode .ro-connector{margin:0 7px 0 0;}
    .ro-connector-current{background:linear-gradient(180deg,#2ecc71,var(--accent));}

    /* .ro-node-body wraps label+sub so the row can hold a third item (the
       chevron) without the dot/label/sub flex math changing. */
    .ro-node-body{flex:1 1 auto; min-width:0;}
    .ro-node-chevron{
      flex:none; margin-top:3px; color:var(--text-dim);
      transition:transform .25s ease, color .2s ease;
    }
    .ro-node-wrap.ro-node-open .ro-node-chevron{transform:rotate(180deg); color:var(--accent);}
    .ro-node-wrap.ro-node-open .ro-node-label{color:var(--accent);}

    /* The category breakdown for a pressed year slides out right under that
       year's own card — an accordion, not a shared panel elsewhere on the
       page (see bindScopeTabs' isPhone branch). Padding animates alongside
       max-height so a collapsed panel takes no space at all, not just
       "no visible content"; .ro-bd-* inside is the exact same breakdown
       markup/CSS the desktop panel already uses. */
    .ro-inline-panel{
      max-height:0; opacity:0; overflow:hidden;
      margin:0 0 0 28px; padding:0 14px;
      background:var(--panel); border:1px solid transparent; border-radius:12px;
      transition:max-height .3s ease, opacity .25s ease, padding .25s ease, margin .25s ease, border-color .25s ease;
    }
    [dir="rtl"] .ro-inline-panel, .plan-page.rtl-mode .ro-inline-panel{margin:0 28px 0 0;}
    .ro-inline-panel-open{
      max-height:2000px; opacity:1; padding:14px; margin:10px 0 14px 28px;
      border-color:var(--line);
    }
    [dir="rtl"] .ro-inline-panel-open, .plan-page.rtl-mode .ro-inline-panel-open{margin:10px 28px 14px 0;}
    /* The breakdown's own ring/figures crowd a 28px-narrower column — same
       shrink the 520px breakpoint already gives the shared panel. */
    .ro-inline-panel .ro-bd-figures{gap:14px;}
    .ro-inline-panel .ro-bd-ring{width:64px; height:64px;}
    .ro-inline-panel .ro-bd-ring span{width:48px; height:48px; font-size:14px;}

    @media (prefers-reduced-motion:reduce){
      .ro-node-chevron, .ro-inline-panel{transition:none;}
    }
  }

  /* ================= PREREQUISITE MAP (what course search opens now) ================= */
  .pg-modal-card{max-width:1200px; width:94vw; max-height:88vh; overflow-y:auto; overflow-x:hidden;}
  .pg-head{margin-bottom:8px;}
  .pg-truncated{background:rgba(255,255,255,.04); border-radius:8px; padding:8px 10px; margin:0 0 14px;}

  .pg-body{display:flex; flex-direction:column; gap:16px;}
  /* 860px, same reasoning as the GPA table's own breakpoint: this lives
     inside a card capped at 94vw, so the viewport width a media query sees
     is never the width this layout actually has to work with. */
  @media (min-width:860px){
    .pg-body{flex-direction:row; align-items:flex-start;}
    .pg-graph-wrap{flex:2.2 1 0; min-width:0;}
    .pg-side{flex:1 1 0; max-width:280px; min-width:0;}
  }

  .pg-graph-wrap{
    overflow:auto; border:1px solid var(--line); border-radius:12px;
    background:rgba(255,255,255,.02); max-height:60vh; cursor:grab;
  }

  /* On a phone the tree is the screen. It used to be sized to its own
     content, which on a short chain is a ~108px letterbox at the top with
     the Selected / Legend / Shortest route panels taking everything below —
     three panels of prose above the thing they describe. The graph now
     claims most of the viewport and the panels sit under it, reached by
     scrolling, which is also what "full screen" asked for. */
  @media (max-width:720px){
    .pg-graph-wrap{
      min-height:56vh; max-height:64vh;
      cursor:default;                    /* no drag-pan; touch scrolls it */
      overscroll-behavior:contain;       /* and does not drag the page with it */
    }
    /* A chain one course wide is the common case, and left-aligning it left
       the tree hugging one edge with the rest of the box empty. Centres when
       it fits; a wider layer still scrolls from its natural left edge. */
    .pg-graph{margin-inline:auto;}
    .pg-node{padding:7px 9px;}
    .pg-node-name{font-size:12px;}
    .pg-node-sub{font-size:9.5px;}
  }
  .pg-graph-wrap.pg-dragging{cursor:grabbing; user-select:none;}
  .pg-graph{position:relative;}
  .pg-svg{position:absolute; top:0; left:0; pointer-events:none;}
  .pg-edge{fill:none; stroke:var(--accent); stroke-width:2;}
  .pg-edge-dashed{stroke:var(--line); stroke-dasharray:5 4;}
  .pg-edge-passed{stroke:#2ecc71;}
  .pg-edge-progress{stroke:var(--accent);}
  .pg-edge-unlocked{stroke:#f5a623;}

  .pg-node{
    position:relative; background:var(--panel); border:1.5px solid var(--line); border-radius:10px;
    padding:8px 10px; cursor:pointer; transition:border-color .15s ease, transform .15s ease;
    /* Fills the box the layout reserved for it, so the height the maths
       assumes and the height on screen are the same number. */
    height:100%; box-sizing:border-box;
    display:flex; flex-direction:column; justify-content:center;
  }
  .pg-node:hover{transform:translateY(-1px);}
  /* Clamped to 2 lines with an ellipsis so a long course name can never grow
     a card taller than the fixed height the (deterministic, non-DOM-measured)
     layout math assumes — without this, a long name wrapped to 3+ lines
     visually collided with whatever card sits below it in the same column. */
  .pg-node-name{
    font-size:12.5px; font-weight:700; color:var(--text); overflow-wrap:anywhere;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  }
  .pg-node-sub{font-size:10px; color:var(--text-dim); margin-top:2px; text-transform:uppercase; letter-spacing:.02em;}
  /* Solid status-color fills (not just a colored border) — this is the exact
     look from the approved mockup: a course's state reads at a glance from
     across the room, not just from the thin edge of its card. */
  /* Fixed (not theme-variable) dark fills with light text, same convention
     as the category colors (.core/.skills/etc) elsewhere — a solid color
     that reads correctly whether the surrounding page is in dark or light
     mode, rather than a translucent tint over the (theme-dependent) panel
     background. */
  .pg-node-passed{background:#1e5c3a; border-color:#2ecc71;}
  .pg-node-passed .pg-node-name{color:#eafff2;}
  .pg-node-passed .pg-node-sub{color:#8ff0bd;}
  .pg-node-in_progress{background:color-mix(in srgb, var(--accent) 30%, var(--panel)); border-color:var(--accent); box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent);}
  .pg-node-in_progress .pg-node-name{color:#eaf6ff;}
  .pg-node-in_progress .pg-node-sub{color:#a8dcff;}
  .pg-node-unlocked{background:#5c3d14; border-color:#f5a623;}
  .pg-node-unlocked .pg-node-name{color:#fff6ea;}
  .pg-node-unlocked .pg-node-sub{color:#ffcf8a;}
  .pg-node-locked{opacity:.62;}
  .pg-node-selected{box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);}

  .pg-side{display:flex; flex-direction:column; gap:12px;}
  .pg-selected, .pg-legend, .pg-route{background:var(--panel); border:1px solid var(--line); border-radius:11px; padding:12px 14px;}
  .pg-panel-label{font-size:10.5px; font-weight:700; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; margin-bottom:6px;}

  .pg-why-locked-btn{
    display:block;margin-top:10px;background:color-mix(in srgb, var(--accent) 12%, transparent);border:1px solid var(--accent);color:var(--accent);
    font-family:inherit;font-size:11.5px;font-weight:800;border-radius:999px;padding:6px 14px;cursor:pointer;
  }
  .pg-why-locked-btn:hover{background:var(--accent);color:var(--on-accent);}

  .pg-legend-row{display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text); padding:4px 0;}
  .pg-legend-dot{width:10px; height:10px; border-radius:50%; flex:none;}

  html[dir="rtl"] .pg-node-sub, .plan-page.rtl-mode .pg-node-sub{direction:rtl;}

  /* ==========================================================================
     13. ACHIEVEMENTS & AI ADVISOR — the achievement badge grid and the "plan my
     next semester" recommendation cards
     ========================================================================== */
  /* ================= ACHIEVEMENTS ================= */

  /* Year-completion "terminal" celebration popup (typewriter). */
  .year-cel-overlay{
    position:fixed;inset:0;z-index:4000;display:flex;align-items:center;justify-content:center;
    background:rgba(6,10,26,.72);backdrop-filter:blur(3px);opacity:0;transition:opacity .3s ease;padding:20px;
  }
  .year-cel-overlay.open{opacity:1;}
  .year-cel-card{
    width:min(520px,100%);background:#0b1020;border:1px solid #26305a;border-radius:12px;
    box-shadow:0 24px 60px rgba(0,0,0,.55);overflow:hidden;transform:translateY(10px) scale(.98);
    transition:transform .3s cubic-bezier(.2,.8,.2,1);
  }
  .year-cel-overlay.open .year-cel-card{transform:none;}
  .year-cel-bar{
    display:flex;align-items:center;gap:7px;padding:9px 13px;background:#141c34;border-bottom:1px solid #26305a;
  }
  .ycb-dot{width:11px;height:11px;border-radius:50%;display:inline-block;}
  .ycb-dot.r{background:#ff5f56;} .ycb-dot.y{background:#ffbd2e;} .ycb-dot.g{background:#27c93f;}
  .ycb-title{margin-left:8px;font:600 12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#8b9ad1;}
  .year-cel-body{
    margin:0;padding:20px 18px 8px;font:600 15px/1.7 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    color:#4ade80;white-space:pre-wrap;word-break:break-word;min-height:96px;
  }
  .ycb-run{color:var(--accent);}
  .year-cel-cursor{color:#4ade80;animation:ycb-blink 1s steps(1) infinite;}
  @keyframes ycb-blink{50%{opacity:0;}}
  .year-cel-close{
    display:block;margin:6px 18px 16px auto;background:#1b2b4a;color:#cfe0ff;
    border:1px solid #2f3f6a;border-radius:7px;padding:6px 16px;cursor:pointer;
    font:600 12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  }
  .year-cel-close:hover{background:#243662;}
  @media (prefers-reduced-motion:reduce){ .year-cel-cursor{animation:none;} }

  .achievements-summary{font-size:12.5px;color:var(--text-dim);margin:0;}
  .ach-summary-row{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin:-6px 0 14px;}
  .ach-grid-save{
    cursor:pointer;font-family:inherit;font-size:11px;font-weight:700;color:var(--text-dim);
    background:transparent;border:1px solid var(--line);border-radius:999px;padding:5px 12px;white-space:nowrap;
  }
  .ach-grid-save:hover{border-color:var(--accent);color:var(--text);}

  /* "Next Up" hero — the locked-but-applicable achievement closest to being
     unlocked, so the modal leads with something concrete to chase instead of
     a wall of badges. Uses the same fixed gold as .achievement-badge.unlocked
     (achievement color stays constant across themes, like other category
     colors in this app) rather than the per-theme --accent. */
  .ach-next{
    display:flex;align-items:center;gap:12px;
    background:linear-gradient(180deg, rgba(242,185,61,.16), rgba(242,185,61,.04));
    border:1px solid #f2b93d;border-radius:14px;
    padding:13px 15px;margin-bottom:14px;
  }
  .ach-next-icon{
    font-size:22px;flex:0 0 auto;width:44px;height:44px;border-radius:12px;
    background:rgba(242,185,61,.16);display:flex;align-items:center;justify-content:center;
  }
  .ach-next-icon .app-icon-svg{width:22px;height:22px;color:#f2b93d;}
  .ach-next-body{flex:1 1 auto;min-width:0;}
  .ach-next-kicker{font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:#f2b93d;}
  .ach-next-title{font-size:13px;font-weight:800;margin-top:2px;color:var(--text);}
  .ach-next-track{margin-top:7px;height:6px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden;}
  .ach-next-track > span{display:block;height:100%;background:#f2b93d;border-radius:3px;transition:width .4s ease;}

  .achievement-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;}

  .achievement-badge{
    background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:10px;
    padding:14px 10px;text-align:center;transition:transform .15s ease;
  }

  .achievement-badge.unlocked{border-color:#f2b93d;background:rgba(242,185,61,.09);}

  .achievement-badge.unlocked:hover{transform:translateY(-2px);}

  .achievement-badge.locked{opacity:.4;filter:grayscale(1);}

  .achievement-badge.na{opacity:.22;}

  .achievement-badge .ab-icon{
    font-size:20px;margin:0 auto 8px;width:42px;height:42px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:rgba(255,255,255,.05);border:2px solid var(--line);
  }
  .achievement-badge .ab-icon .app-icon-svg{width:20px;height:20px;color:var(--text-dim);}

  .achievement-badge.unlocked .ab-icon{border-color:#f2b93d;background:rgba(242,185,61,.16);}
  .achievement-badge.unlocked .ab-icon .app-icon-svg{color:#f2b93d;}

  .achievement-badge .ab-title{font-size:11.5px;font-weight:700;color:var(--text);margin-bottom:4px;line-height:1.3;}

  .achievement-badge .ab-desc{font-size:10px;color:var(--text-dim);line-height:1.4;}

  .achievement-badge .ab-tag{font-size:9px;font-weight:700;margin-top:6px;color:#f2b93d;}

  .achievement-badge.na .ab-tag{color:var(--text-dim);}

  .achievement-badge .ab-progress{margin-top:8px;width:100%;}
  .achievement-badge .ab-progress-track{
    height:5px;border-radius:3px;background:rgba(255,255,255,.09);overflow:hidden;
  }
  .achievement-badge .ab-progress-track > span{
    display:block;height:100%;border-radius:3px;background:var(--accent);
    transition:width .4s ease;
  }
  .achievement-badge .ab-progress-text{
    font-size:9px;font-weight:700;color:var(--accent);margin-top:4px;
  }

  .achievement-badge .ab-share{
    margin-top:8px;background:color-mix(in srgb, var(--accent) 14%, transparent);border:1px solid var(--accent);
    color:var(--accent);font-family:inherit;font-size:10px;font-weight:800;
    border-radius:999px;padding:4px 12px;cursor:pointer;
  }
  .achievement-badge .ab-share:hover{background:var(--accent);color:var(--on-accent);}

  /* Filter chips, section labels and the "Recently unlocked" strip are phone-only
     structural additions (mockup-driven); desktop keeps its plain single-grid
     layout, so these stay hidden there. */
  .ach-cats, .ach-section-label, .ach-recent-scroll{display:none;}

  @media (max-width:720px){
    .ach-cats{display:flex;gap:7px;overflow-x:auto;-webkit-overflow-scrolling:touch;margin:0 0 14px;padding-bottom:2px;}
    .ach-cat-pill{
      flex:0 0 auto;font-size:11.5px;font-weight:700;padding:6px 13px;border-radius:999px;
      border:1px solid var(--line);background:none;color:var(--text-dim);
      font-family:inherit;cursor:pointer;white-space:nowrap;
    }
    .ach-cat-pill:hover{border-color:var(--accent);color:var(--text);}
    .ach-cat-pill.active{background:var(--accent);border-color:var(--accent);color:var(--on-accent);}

    .ach-section-label{
      display:block;font-size:10.5px;font-weight:800;color:var(--text-dim);text-transform:uppercase;
      letter-spacing:.06em;margin:16px 2px 8px;
    }

    .ach-recent-scroll{display:flex;gap:9px;overflow-x:auto;-webkit-overflow-scrolling:touch;margin:0 0 4px;padding-bottom:2px;}
    .ach-recent-card{
      flex:0 0 auto;width:118px;background:rgba(255,255,255,.03);border:1px solid var(--line);
      border-radius:12px;padding:11px;
    }
    .ach-recent-icon{
      width:32px;height:32px;border-radius:50%;margin-bottom:8px;
      display:flex;align-items:center;justify-content:center;
      background:rgba(242,185,61,.16);border:2px solid #f2b93d;
    }
    .ach-recent-icon .app-icon-svg{width:17px;height:17px;color:#f2b93d;}
    .ach-recent-title{font-size:11px;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:3px;}
    .ach-recent-when{font-size:9.5px;color:var(--text-dim);}
  }
  /* ================= AI STUDY ADVISOR ================= */

  /* ---------- the travelling progress meter (js/69-phone-header.js) ----------
     The plan's hours bar sticks to the top of the scroll and shrinks once
     it gets there: caption gone, padding down, so it costs a strip rather
     than a block while a student scrolls through course cards.

     .imp-body-pad is the scroll container it sticks within, and it wraps
     everything from the description down to the last year — which is why
     the meter can travel the whole length of the plan. A sticky element
     only moves inside its own parent's box; pinning it to a wrapper that
     is exactly its own height (which is what the old sticky meter in Plan
     My Next Semester did) means it never moves at all.

     Everything the old phone header built here — the collapsing chrome
     bar, the Y1..Y5 jump tabs, the Legend and "Available only" chips and
     the jump-to-current button — is gone. The buttons those chips stood in
     for moved to the menu, and every year folds on its own now, so there
     is nothing left to collapse behind a chevron. */
  .progress-widget{
    position:sticky;top:0;z-index:30;
    transition:padding .18s ease, box-shadow .18s ease, border-radius .18s ease;
  }
  /* Sticky against a transparent background would let course cards scroll
     through the meter, so it carries its own ground once it is stuck. */
  .sheet-plan.pw-stuck .progress-widget{
    background:var(--panel);
    box-shadow:0 10px 22px -14px rgba(0,0,0,.75);
  }
  .sheet-plan.pw-stuck .progress-widget .pw-gate-caption{
    display:none;
  }
  @media (prefers-reduced-motion:reduce){
    .progress-widget{transition:none;}
  }
  @media (max-width:720px){
    /* Narrower and shorter on a phone, and pulled to the full width of the
       page so the stuck bar spans the screen rather than floating with a
       margin either side of it. */
    .sheet-plan.pw-stuck .progress-widget{
      margin-inline:0;border-radius:0;padding:8px 14px;
    }
  }

  /* ---------- skeleton loading (js/70-skeleton.js) ----------
     A slow diagonal shimmer sweeping across a flat placeholder — the
     standard "something is coming" cue, built from one gradient and one
     animation so it costs nothing to add to a new screen later. */
  .skel-wrap{display:flex;flex-direction:column;gap:10px;}
  .skel-card{display:flex;gap:10px;align-items:center;padding:10px;border:1px solid var(--line);border-radius:10px;}
  .skel-avatar{width:34px;height:34px;border-radius:50%;flex:0 0 auto;}
  .skel-lines{flex:1 1 auto;display:flex;flex-direction:column;gap:7px;}
  .skel-line{height:9px;border-radius:5px;}
  .skel-line-a{width:55%;}
  .skel-line-b{width:80%;}
  .skel-avatar,.skel-line{
    background:linear-gradient(100deg,var(--line) 30%,var(--search-bg) 50%,var(--line) 70%);
    background-size:250% 100%;
    animation:skelShimmer 1.4s ease-in-out infinite;
  }
  @keyframes skelShimmer{
    0%{background-position:150% 0;}
    100%{background-position:-50% 0;}
  }
  .skel-tip{
    margin:6px 0 0;font-size:11.5px;color:var(--text-dim);
    background:var(--surface-bg);border:1px solid var(--line);border-radius:8px;
    padding:8px 11px;
  }
  @media (prefers-reduced-motion:reduce){
    .skel-avatar,.skel-line{animation:none;background:var(--line);}
  }

  /* ---------- share link + QR (js/72-share.js) ---------- */
  .share-loading{font-size:12.5px;color:var(--text-dim);}
  .share-native-btn{
    /* width:100% was for when this button owned its own row; it now shares
       one with Copy link, and flex sizing in .share-actions does the work. */
    display:flex;justify-content:center;margin:0;
    background:var(--accent);color:var(--on-accent);border-color:var(--accent);font-weight:800;
  }
  .share-link-row{display:flex;gap:8px;margin:14px 0 0;}
  .share-link-input{
    flex:1 1 auto;min-width:0;padding:9px 12px;border:1px solid var(--line);border-radius:8px;
    background:var(--search-bg);color:var(--text);font-family:monospace;font-size:12px;
  }
  /* The QR opens the screen. It is the one part of sharing that works
     across a room, and it used to sit last, under a paragraph, a button and
     a URL field — below the fold on a phone and captioned like an
     afterthought. */
  .share-qr-wrap{
    text-align:center;padding:20px 16px 16px;border:1px solid var(--line);
    border-radius:16px;background:var(--surface-bg);margin:4px 0 0;
    display:flex;flex-direction:column;align-items:center;gap:10px;
  }
  /* A QR needs a light field around it to scan, in every theme — so the
     plate is white regardless of which one is on, not a themed surface. */
  .share-qr-plate{
    background:#fff;border-radius:14px;padding:12px;line-height:0;
    box-shadow:0 6px 22px rgba(0,0,0,.28);
  }
  .share-qr-canvas{max-width:100%;height:auto;display:block;}
  .share-qr-for{
    font-size:14px;font-weight:800;color:var(--text);margin:0;line-height:1.3;
    overflow-wrap:anywhere;
  }
  .share-qr-label{
    font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
    color:var(--text-dim);margin:0;
  }
  .share-qr-toobig{font-size:12.5px;color:var(--text-dim);margin:0;}

  /* The two things most people actually came to do, directly under the code
     and above the link field they mostly do not need. */
  .share-actions{display:flex;gap:8px;margin:14px 0 0;}
  .share-actions .home-btn{flex:1;justify-content:center;}
  .share-lead{margin:16px 0 0;}

  /* ---------- contacts (js/68-contacts.js) ---------- */
  .ct-privacy{
    font-size:11px;color:var(--text-dim);background:var(--surface-bg);
    border:1px solid var(--line);border-radius:8px;padding:8px 11px;margin:0 0 12px;
  }
  .ct-loading{font-size:12.5px;color:var(--text-dim);}
  .ct-search{
    width:100%;font-family:inherit;font-size:13px;padding:9px 12px;
    border-radius:10px;border:1px solid var(--line);background:var(--search-bg);
    color:var(--text);margin-bottom:10px;
  }
  .ct-chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px;}
  .ct-chip{
    font-size:11.5px;font-weight:700;padding:6px 13px;border-radius:999px;
    border:1px solid var(--line);background:none;color:var(--text-dim);
    font-family:inherit;cursor:pointer;white-space:nowrap;
  }
  .ct-chip:hover{border-color:var(--accent);color:var(--text);}
  .ct-chip-on{background:var(--accent);border-color:var(--accent);color:var(--on-accent);}
  .ct-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px;}

  /* Directory sections. Same headed-shelf treatment the Course Library uses,
     so two lists in the same app that group things group them the same way.
     The header sticks while its own section scrolls — on a long section the
     heading leaves the screen well before its last card does, and after
     that every card looks alike again. */
  .ct-group + .ct-group{margin-top:16px;}
  .ct-group-head{
    position:sticky; top:0; z-index:1;
    display:flex; align-items:baseline; justify-content:space-between; gap:10px;
    padding:6px 0 5px; margin-bottom:8px;
    background:var(--panel); border-bottom:1px solid var(--accent);
  }
  .ct-group-label{
    font-size:10.5px; font-weight:800; letter-spacing:.06em;
    text-transform:uppercase; color:var(--accent);
  }
  .ct-group-count{
    font-size:10.5px; font-weight:700; color:var(--text-dim);
    font-variant-numeric:tabular-nums;
  }
  .ct-card{border:1px solid var(--line);border-radius:12px;padding:12px;background:var(--panel);}
  .ct-card-top{display:flex;align-items:flex-start;gap:10px;margin-bottom:8px;}
  .ct-avatar{
    width:34px;height:34px;flex:0 0 34px;border-radius:50%;background:var(--surface-bg);
    border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-size:16px;
  }
  .ct-card-main{display:flex;flex-direction:column;gap:2px;min-width:0;}
  .ct-name{font-size:13px;font-weight:800;color:var(--text);line-height:1.3;}
  .ct-sub{font-size:11px;color:var(--text-dim);line-height:1.4;}
  .ct-email-row{display:flex;align-items:center;gap:8px;justify-content:space-between;}
  .ct-email{
    font-size:11.5px;color:var(--accent);text-decoration:none;overflow:hidden;
    text-overflow:ellipsis;white-space:nowrap;min-width:0;
  }
  .ct-email:hover{text-decoration:underline;}
  .ct-copy{
    font-size:10.5px;font-weight:700;color:var(--text-dim);background:none;
    border:1px solid var(--line);border-radius:999px;padding:3px 10px;
    font-family:inherit;cursor:pointer;flex:0 0 auto;
  }
  .ct-copy:hover{border-color:var(--accent);color:var(--text);}
  .ct-noemail{font-size:11px;color:var(--text-dim);font-style:italic;}
  .ct-empty{font-size:12.5px;color:var(--text-dim);margin-top:10px;}

  /* ---------- when do I graduate (js/66-graduation.js) ---------- */
  .grad-msg{font-size:12.5px;color:var(--text-dim);margin:0;}
  .grad-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;}
  .grad-load-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);}
  .grad-load-val{font-size:14px;font-weight:800;color:var(--accent);}
  .grad-slider{
    -webkit-appearance:none;appearance:none;width:100%;height:8px;border-radius:999px;
    background:linear-gradient(to right,var(--unlock),var(--accent));cursor:pointer;margin-bottom:14px;
  }
  .grad-slider::-webkit-slider-thumb{
    -webkit-appearance:none;width:18px;height:18px;border-radius:50%;
    background:#fff;border:2px solid var(--accent);cursor:pointer;
  }
  .grad-slider::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#fff;border:2px solid var(--accent);cursor:pointer;}
  .grad-dots{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px;position:relative;}
  .grad-dots::before{
    content:'';position:absolute;top:5px;left:5%;right:5%;height:2px;background:var(--line);z-index:0;
  }
  .grad-dot{display:flex;flex-direction:column;align-items:center;gap:5px;flex:1;position:relative;z-index:1;}
  .grad-dot-mark{width:10px;height:10px;border-radius:50%;background:var(--line);border:2px solid var(--surface-bg);}
  .grad-dot-now .grad-dot-mark{background:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);}
  .grad-dot-end .grad-dot-mark{background:var(--unlock);}
  /* Only the two ends carry a label; the empty ones in between must not
     stretch the flex row, so the text can overflow its own dot. */
  .grad-dot-label{font-size:9.5px;color:var(--text-dim);text-align:center;white-space:nowrap;}
  .grad-dot:first-child .grad-dot-label{text-align:start;}
  .grad-dot:last-child .grad-dot-label{text-align:end;}
  .grad-dot-now .grad-dot-label{color:var(--accent);font-weight:700;}
  /* The date the card exists to give, now the first thing in it. */
  .grad-finish{display:flex;align-items:baseline;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:14px;}
  /* Direct child only: the years hint is a span too, nested inside the <b>,
     and the eyebrow's uppercase was leaking onto it. */
  .grad-finish > span{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);}
  .grad-finish b{font-size:17px;font-weight:800;color:var(--unlock);}
  .grad-years-hint{font-size:12px;font-weight:700;color:var(--text-dim);}
  .grad-note{margin:10px 0 0;font-size:12px;line-height:1.6;color:var(--text-dim);}
  .grad-note-bound{color:var(--text);}
  .grad-caveat{margin:10px 0 0;font-size:10.5px;color:var(--text-dim);opacity:.8;line-height:1.5;}
  .grad-save-img{
    margin-top:10px;cursor:pointer;font-family:inherit;font-size:11px;font-weight:700;
    color:var(--text-dim);background:transparent;border:1px solid var(--line);border-radius:999px;padding:5px 12px;
  }
  .grad-save-img:hover{border-color:var(--accent);color:var(--text);}

  /* ---------- gpa target (js/67-gpa-target.js) ---------- */
  .gt-section{
    margin:18px 0 20px;padding:14px 16px;border-radius:12px;
    background:var(--surface-bg);border:1px solid var(--line);
  }
  .gt-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:8px;}
  .gt-now{font-size:12px;color:var(--text-dim);}
  .gt-now b{font-size:15px;color:var(--text);}
  .gt-target{font-size:12px;color:var(--text-dim);text-align:right;}
  .gt-target b{font-size:15px;color:var(--accent);}
  .gt-slider{
    -webkit-appearance:none;appearance:none;width:100%;height:8px;border-radius:999px;
    background:linear-gradient(to right,var(--prereq),var(--connect) 50%,var(--unlock));cursor:pointer;margin:6px 0 10px;
  }
  .gt-slider::-webkit-slider-thumb{
    -webkit-appearance:none;width:18px;height:18px;border-radius:50%;
    background:#fff;border:2px solid var(--accent);cursor:pointer;
  }
  .gt-slider::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#fff;border:2px solid var(--accent);cursor:pointer;}
  .gt-msg{font-size:12.5px;color:var(--text-dim);line-height:1.6;margin:0 0 10px;}
  .gt-msg b{color:var(--text);}
  .gt-ok{color:var(--unlock);font-weight:700;}
  .gt-warn{color:var(--prereq);font-weight:700;}
  .gt-presets{display:flex;gap:8px;flex-wrap:wrap;}
  .gt-preset{
    font-size:11px;font-weight:700;padding:5px 12px;border-radius:999px;
    border:1px solid var(--line);background:none;color:var(--text-dim);font-family:inherit;cursor:pointer;
  }
  .gt-preset:hover{border-color:var(--accent);color:var(--text);}
  .gt-preset-ok{color:var(--unlock);border-color:var(--unlock);}
  .gt-preset-bad{color:var(--prereq);border-color:var(--prereq);}

  /* ---------- what-if GPA (js/63-whatif.js) ----------
     Two big numbers with an arrow between them, then the rows that move the
     second one. The arrow colours by direction so the answer is legible
     before the digits are read. */
  .wi-head{
    display:flex;align-items:stretch;gap:10px;margin:12px 0 6px;
    border:1px solid var(--line);border-radius:12px;padding:12px;
    background:var(--surface-bg);
  }
  .wi-fig{flex:1;display:flex;flex-direction:column;gap:2px;text-align:center;}
  .wi-fig span{font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--text-dim);}
  .wi-fig b{font-size:26px;font-weight:800;color:var(--text);line-height:1.15;}
  .wi-fig small{font-size:10.5px;color:var(--text-dim);}
  .wi-arrow{display:flex;align-items:center;font-size:20px;color:var(--text-dim);}
  .wi-up .wi-arrow,.wi-up .wi-fig:last-child b{color:var(--unlock);}
  .wi-down .wi-arrow,.wi-down .wi-fig:last-child b{color:var(--prereq);}
  .wi-h3{margin:18px 0 4px;font-size:13px;font-weight:800;color:var(--text);}
  .wi-list{display:flex;flex-direction:column;gap:6px;margin-top:8px;}
  .wi-row{
    display:flex;align-items:center;gap:9px;padding:7px 10px;
    border:1px solid var(--line);border-radius:9px;background:var(--panel);
  }
  .wi-row-name{flex:1;min-width:0;font-size:12.5px;font-weight:700;color:var(--text);line-height:1.35;}
  .wi-row-cr{font-size:11px;font-weight:800;color:var(--accent);}
  .wi-row-old{
    font-size:11px;font-weight:800;color:var(--text-dim);
    border:1px solid var(--line);border-radius:6px;padding:1px 6px;
  }
  .wi-sel{
    font-family:inherit;font-size:12px;padding:5px 8px;border-radius:7px;
    border:1px solid var(--line);background:var(--search-bg);color:var(--text);
    max-width:44%;
  }
  .wi-msg{font-size:12px;color:var(--text-dim);line-height:1.6;margin:8px 0 0;}
  .wi-ok{color:var(--unlock);font-weight:700;}
  .wi-warn{color:var(--prereq);font-weight:700;}
  .wi-target{display:flex;align-items:flex-start;gap:10px;flex-wrap:wrap;margin-top:8px;}
  .wi-target input{
    width:88px;font-family:inherit;font-size:15px;font-weight:800;padding:7px 9px;
    border-radius:8px;border:1px solid var(--line);background:var(--search-bg);color:var(--text);
  }
  .wi-target > div{flex:1;min-width:180px;}
  .wi-more{
    margin-top:8px;background:none;border:0;padding:4px 0;cursor:pointer;
    font-family:inherit;font-size:12px;font-weight:800;color:var(--accent);
  }
  .wi-actions{display:flex;gap:8px;margin-top:14px;}

  /* ---------- change major (js/62-change-plan.js) ----------
     A ranked list of every other plan with how much of your work carries
     into it, then one plan's detail. The bar in each row is relative to the
     best match, so the list reads at a glance as "this is where my credits
     go furthest" without anyone doing arithmetic. */
  .cp-current{
    display:flex;align-items:center;gap:8px;flex-wrap:wrap;
    border:1px solid var(--line);border-radius:10px;padding:9px 12px;
    background:var(--surface-bg);margin:10px 0 14px;font-size:12px;color:var(--text-dim);
  }
  .cp-current b{color:var(--text);font-size:13px;}
  .cp-list{display:flex;flex-direction:column;gap:8px;}
  .cp-row{
    display:flex;align-items:center;gap:11px;width:100%;text-align:start;
    padding:10px 12px;border-radius:11px;border:1px solid var(--line);
    background:var(--panel);cursor:pointer;font-family:inherit;color:var(--text);
    transition:border-color .13s ease;
  }
  .cp-row:hover{border-color:var(--accent);}
  .cp-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .cp-row-icon{font-size:22px;flex:0 0 auto;line-height:1;}
  .cp-row-main{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1;}
  .cp-row-name{font-size:13px;font-weight:700;line-height:1.3;}
  .cp-row-col{font-size:10.5px;color:var(--text-dim);}
  .cp-bar{
    display:block;height:5px;border-radius:999px;margin-top:3px;
    background:var(--search-bg);border:1px solid var(--line);overflow:hidden;
  }
  .cp-bar-fill{display:block;height:100%;background:var(--unlock);}
  .cp-row-num{display:flex;flex-direction:column;align-items:flex-end;flex:0 0 auto;}
  .cp-row-num b{font-size:15px;font-weight:800;color:var(--accent);line-height:1.1;}
  .cp-row-num small{font-size:9.5px;color:var(--text-dim);letter-spacing:.03em;}
  .cp-back{
    background:none;border:0;color:var(--text-dim);font-family:inherit;
    font-size:12px;font-weight:700;cursor:pointer;padding:2px 0;margin-bottom:8px;
  }
  .cp-back:hover{color:var(--text);}
  .cp-head{display:flex;align-items:center;gap:12px;margin-bottom:12px;}
  .cp-head-icon{font-size:30px;line-height:1;}
  .cp-head h3{margin:0;font-size:17px;font-weight:800;color:var(--text);}
  .cp-head p{margin:2px 0 0;font-size:11.5px;color:var(--text-dim);}
  .cp-nums{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-bottom:12px;}
  .cp-num{
    border:1px solid var(--line);border-radius:10px;padding:10px;
    background:var(--surface-bg);text-align:center;
  }
  .cp-num b{display:block;font-size:20px;font-weight:800;color:var(--text);line-height:1.2;}
  .cp-num span{display:block;font-size:10px;color:var(--text-dim);margin-top:3px;line-height:1.45;}
  .cp-det{
    border:1px solid var(--line);border-radius:10px;padding:9px 11px;margin-bottom:8px;
    background:var(--panel);
  }
  .cp-det summary{cursor:pointer;font-size:12px;font-weight:800;color:var(--text);}
  .cp-ul{list-style:none;margin:8px 0 0;padding:0;display:flex;flex-direction:column;gap:5px;}
  .cp-ul li{display:flex;align-items:baseline;gap:8px;font-size:12px;color:var(--text);flex-wrap:wrap;}
  .cp-li-cr{font-size:11px;font-weight:800;color:var(--accent);}
  .cp-li-note{font-size:10.5px;color:var(--text-dim);}
  .cp-ul-lose li{color:var(--text-dim);}
  /* Courses the app could not attempt a match on at all — a question for
     the registrar, not a loss (js/62-change-plan.js). */
  .cp-ul-ask li{color:var(--text-dim);}
  .cp-det-ask > summary{color:var(--warn);}
  .cp-carry{
    display:flex;align-items:flex-start;gap:8px;margin:12px 0 4px;
    font-size:12px;font-weight:700;color:var(--text);line-height:1.5;cursor:pointer;
  }
  .cp-carry input{margin-top:2px;accent-color:var(--accent);}
  .cp-actions{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;}
  .cp-go{border-color:var(--accent);}

  @media (max-width:620px){
    .cp-nums{grid-template-columns:1fr;}
  }

  @media (max-width:720px){
    .cp-current, .cp-row, .cp-num, .cp-det{
      border:none; box-shadow:0 4px 14px rgba(0,0,0,.22);
    }
    .cp-current{border-radius:14px;}
    .cp-row{border-radius:14px;}
    .cp-row:hover{box-shadow:0 4px 14px rgba(0,0,0,.22), 0 0 0 1px var(--accent);}
    .cp-num{border-radius:12px;}
    .cp-det{border-radius:12px;}
  }

  /* ---------- semester builder (js/25-advisor.js) ----------
     Was a read-only list of eight courses; a student picking a semester is
     choosing BETWEEN courses, so this is a pool on one side, a tray on the
     other, and a credit meter over both. Two columns where there is room,
     one stacked column on a phone with the tray on top — the running total
     is the thing you keep glancing at, so it must not be below the fold. */
  .sb-lead{margin-top:0;}
  /* Sticky: the running total is the thing you keep glancing at while
     picking courses, and it used to scroll away with the rest of the page —
     by the time you were choosing the twelfth course you could no longer see
     what it added up to. It shrinks as it sticks (see .sb-meter-stuck) so it
     costs a strip rather than a card once it is doing that job. */
  /* The HOST sticks, not the meter. position:sticky travels only within its
     own parent's box, and the host is exactly the meter's height — so
     sticking the meter to it meant sticking it to nothing and leaving with
     it. The host is a direct child of the modal's scroll box, which is the
     full height of the content. */
  #sbMeterHost{position:sticky; top:0; z-index:5;}
  .sb-meter{
    border:1px solid var(--line);border-radius:12px;padding:12px 14px;
    background:var(--surface-bg);margin-bottom:14px;
    transition:padding .16s ease, box-shadow .16s ease, border-radius .16s ease;
  }
  .sb-meter-stuck{
    padding:8px 12px; border-radius:10px;
    box-shadow:0 6px 18px rgba(0,0,0,.28);
  }
  .sb-meter-stuck .sb-meter-num{font-size:19px;}
  .sb-meter-stuck .sb-meter-bar{height:7px;margin-top:6px;}
  .sb-meter-stuck .sb-meter-scale{display:none;}
  @media (prefers-reduced-motion:reduce){ .sb-meter{transition:none;} }
  .sb-meter-top{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;}
  .sb-meter-num{font-size:26px;font-weight:800;color:var(--text);line-height:1.1;}
  .sb-meter-note{font-size:12px;color:var(--text-dim);font-weight:600;}
  .sb-meter-bar{
    position:relative;height:10px;border-radius:999px;margin-top:9px;
    background:var(--search-bg);border:1px solid var(--line);overflow:hidden;
  }
  .sb-meter-fill{
    position:absolute;inset-block:0;inset-inline-start:0;border-radius:999px;
    background:var(--text-dim);transition:width .18s ease,background-color .18s ease;
  }
  .sb-meter-good .sb-meter-fill{background:var(--unlock);}
  .sb-meter-over .sb-meter-fill{background:var(--prereq);}
  .sb-meter-low .sb-meter-fill{background:var(--connect);}
  /* The two ticks are where 12H and 18H fall, so the bar reads as a scale
     rather than as a bar that happens to be part-full. */
  .sb-meter-tick{position:absolute;inset-block:0;width:1px;background:var(--line);}
  .sb-meter-scale{
    display:flex;justify-content:space-between;margin-top:4px;
    font-size:10px;font-weight:700;color:var(--text-dim);letter-spacing:.04em;
  }
  .sb-cols{display:grid;grid-template-columns:1.35fr .85fr;gap:14px;align-items:start;}
  .sb-pool h3,.sb-tray h3{
    margin:0 0 8px;font-size:11px;font-weight:800;letter-spacing:.07em;
    text-transform:uppercase;color:var(--text-dim);
  }
  .sb-count{
    display:inline-block;margin-inline-start:5px;padding:1px 7px;border-radius:999px;
    background:var(--search-bg);border:1px solid var(--line);color:var(--text);font-size:10px;
  }
  .sb-pool-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px;}
  .sb-pool-head h3{margin:0;flex:1 1 auto;}
  .sb-search{
    flex:0 1 190px;font-family:inherit;font-size:12.5px;padding:7px 10px;
    border-radius:8px;border:1px solid var(--line);
    background:var(--search-bg);color:var(--text);
  }
  .sb-rows{display:flex;flex-direction:column;gap:7px;max-height:52vh;overflow-y:auto;padding-inline-end:2px;}
  .sb-row{
    display:flex;align-items:center;gap:10px;width:100%;text-align:start;
    padding:9px 11px;border-radius:10px;border:1px solid var(--line);
    background:var(--panel);cursor:pointer;font-family:inherit;color:var(--text);
    transition:border-color .13s ease,background-color .13s ease;
  }
  .sb-row:hover{border-color:var(--accent);}
  .sb-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .sb-row-on{border-color:var(--unlock);background:rgba(47,191,113,.10);}
  .sb-row-tick{
    display:inline-flex;align-items:center;justify-content:center;
    width:24px;height:24px;flex:0 0 24px;border-radius:50%;
    border:1px solid var(--line);font-size:13px;font-weight:800;color:var(--text-dim);
  }
  .sb-row-on .sb-row-tick{background:var(--unlock);border-color:var(--unlock);color:var(--on-accent);}
  .sb-row-main{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1;}
  .sb-row-name{font-size:13px;font-weight:700;line-height:1.3;}
  .sb-row-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:10.5px;color:var(--text-dim);}
  .sb-row-cr{font-size:12px;font-weight:800;color:var(--accent);flex:0 0 auto;}
  .sb-chip{
    display:inline-block;padding:1px 7px;border-radius:999px;
    border:1px solid var(--line);font-weight:700;white-space:nowrap;
  }
  .sb-chip-unlock{color:var(--unlock);border-color:var(--unlock);}
  .sb-chip-hard{color:var(--prereq);border-color:var(--prereq);}
  .sb-chip-core{color:var(--core);}   .sb-chip-math{color:var(--math);}
  .sb-chip-dept{color:var(--dept);}   .sb-chip-uni{color:var(--uni);}
  .sb-chip-free{color:var(--free);}   .sb-chip-skills{color:var(--skills);}
  .sb-chip-eng{color:var(--eng);}
  .sb-tray{
    border:1px solid var(--line);border-radius:12px;padding:12px;
    background:var(--surface-bg);position:sticky;top:0;
  }
  .sb-tray-list{display:flex;flex-direction:column;gap:6px;}
  .sb-tray-item{
    display:flex;align-items:flex-start;gap:8px;padding:8px 9px;
    border-radius:8px;background:var(--panel);border:1px solid var(--line);
  }
  .sb-tray-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
  .sb-tray-name{min-width:0;font-size:12px;font-weight:700;line-height:1.35;}
  /* The argument for the pick, in the terms the recommender actually ranks
     by. Deliberately quieter than the name: it is there to be checked, not
     read first. */
  .sb-tray-why{font-size:10.5px;color:var(--text-dim);line-height:1.4;}
  .sb-tray-caution{font-size:10.5px;color:var(--text);line-height:1.4;opacity:.85;}
  .sb-tray-cr{font-size:11px;font-weight:800;color:var(--accent);flex:0 0 auto;padding-top:1px;}
  .sb-tray-x{
    background:none;border:0;color:var(--text-dim);cursor:pointer;
    font-size:13px;line-height:1;padding:3px;font-family:inherit;
  }
  .sb-tray-x:hover{color:var(--prereq);}
  .sb-empty{font-size:12px;color:var(--text-dim);margin:6px 0;line-height:1.6;}
  .sb-breakdown{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;font-size:10.5px;color:var(--text-dim);}
  .sb-bd{display:inline-flex;align-items:center;gap:5px;}
  .sb-bd-chip{width:9px;height:9px;border-radius:3px;background:currentcolor;}
  .sb-warn{
    margin-top:10px;border:1px solid var(--prereq);border-radius:9px;
    padding:8px 10px;font-size:11.5px;line-height:1.6;color:var(--text);
  }
  .sb-warn p{margin:0 0 4px;}
  .sb-warn p:last-child{margin-bottom:0;}
  .sb-actions{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;}

  @media (max-width:820px){
    .sb-cols{grid-template-columns:1fr;}
    /* Tray first on a phone: the running total is what you keep checking. */
    .sb-tray{order:-1;position:static;}
    .sb-rows{max-height:none;}
  }

  .advisor-summary{
    background:color-mix(in srgb, var(--accent) 8%, transparent);border:1px solid var(--accent);border-radius:10px;
    padding:11px 14px;margin-bottom:12px;font-size:13px;color:var(--text);
  }

  .advisor-warning{
    background:rgba(224,168,63,.12);border:1px solid var(--connect);border-radius:10px;
    padding:10px 14px;margin-bottom:14px;font-size:12.5px;color:var(--connect);line-height:1.5;
  }

  .advisor-course-list{display:flex;flex-direction:column;gap:10px;}

  .advisor-course-card{background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:10px;padding:12px 14px;}

  .advisor-course-card .acc-head{display:flex;justify-content:space-between;align-items:center;font-weight:700;color:var(--text);margin-bottom:5px;gap:10px;}

  .advisor-course-card .acc-cr{font-size:11px;color:var(--accent);font-weight:700;flex:0 0 auto;}

  .advisor-course-card .acc-reason{font-size:12px;color:var(--text-dim);line-height:1.6;}

  /* ==========================================================================
     14. PERSONALIZATION & COMMUNITY DATA — the small badge row a course card
     shows once it has a personal rating/note, and the equivalent read-only
     badges for anonymous community-submitted feedback
     ========================================================================== */

  .course-meta-badges{
    position:absolute;left:4px;right:4px;bottom:3px;
    display:flex;gap:4px;flex-wrap:wrap;pointer-events:none;z-index:2;
  }

  .meta-badge{
    background:rgba(0,0,0,.45);color:#fff;border-radius:5px;
    font-size:9.5px;font-weight:700;padding:1.5px 5px;line-height:1.5;
    white-space:nowrap;
  }

  .plan-page.rtl-mode .course-meta-badges{direction:rtl;}
  /* ================= COMMUNITY DATA (anonymous, aggregated) ================= */

  .community-badge-row{
    position:absolute;left:4px;top:4px;max-width:75%;
    display:flex;gap:4px;flex-wrap:wrap;z-index:3;pointer-events:none;
  }

  .community-badge-row .meta-badge{pointer-events:auto;cursor:pointer;background:rgba(0,0,0,.55);}

  .community-notes-popover{
    position:fixed;z-index:1300;max-width:260px;background:var(--panel);border:1px solid var(--accent);
    border-radius:10px;padding:10px 12px;font-size:11.5px;color:var(--text);box-shadow:0 8px 28px rgba(0,0,0,.4);
  }

  .community-notes-popover .cnp-note{color:var(--text-dim);margin-bottom:6px;line-height:1.5;font-style:italic;}

  .community-notes-popover .cnp-close{float:right;cursor:pointer;color:var(--text-dim);background:none;border:none;font-size:14px;}

  /* ==========================================================================
     14b. FIRST-RUN LANDING — the first screen a new student sees
     (js/55-onboarding.js). The name, one line, and the choice of signing in.
     Full-bleed card (no padding of its own); .wiz-landing and .wiz-body
     carry the padding.
     ========================================================================== */

  .onboarding-modal-card{max-width:420px;padding:0;overflow:hidden;}

  /* The landing screen. Deliberately tall and mostly empty: the wordmark is
     the content, and the two buttons are the only things to do. */
/* The campus, drawn. Sits behind the landing content and is never in the way
   of a tap. It is a picture rather than a stroked SVG, so its own colours are
   the ones it ships with — no theme colour, no mask fill. */
.wiz-campus{
  /* The whole picture, always. It was scaled to a minimum width before, which
     on a narrow phone pushed the sides past the card and cut the tower in
     half; object-fit:contain keeps the drawing whole at any size and lets the
     landing decide how big it gets. Anchored to the bottom so the walkway
     runs off the bottom edge the way it does in the drawing. */
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:contain; object-position:center bottom;
  /* Close to full strength: the drawing's own ground is within a shade of the
     card behind it, so the alpha is really only softening the linework. */
  opacity:.9; z-index:0;
  pointer-events:none; user-select:none;

  /* The drawing carries its own flat ground, so wherever contain leaves a gap
     the picture would end on a hard edge against the card. Fading the outer
     band on every side turns that edge into a vignette instead. */
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, #000 9%, #000 100%),
    linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
  -webkit-mask-composite:source-in;
  mask-image:
    linear-gradient(to bottom, transparent 0, #000 9%, #000 100%),
    linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-composite:intersect;
}
/* The campus rebuilt as geometry (js/86-campus3d.js). Sits exactly where the
   picture sits, and is transparent until WebGL is actually drawing — the
   .is-live class is set by the module only after the shaders have compiled,
   linked and produced a frame, so a device that cannot run it simply shows
   the drawing underneath and nobody sees a blank rectangle. */
.wiz-campus-3d{
  position:absolute; inset:0; width:100%; height:100%;
  z-index:0; opacity:0; pointer-events:none;
  transition:opacity .9s ease;
}
.wiz-campus-3d.is-live{
  opacity:1;
  /* It reads the pointer for the lean, so it has to be able to receive the
     move — but never a tap: the buttons above it are the whole screen. */
  pointer-events:auto;
}
/* Once the flight has landed the scene turns: drag anywhere on the campus
   and the camera swings round the fountain. touch-action has to go off or
   the browser claims the swipe as a scroll before we see it. */
/* ---- the loading sequence ------------------------------------------------
   Shown before a line of WebGL has run, and it has to hold on its own if
   WebGL never runs at all. The identity first, then what is happening, then
   a bar that fills as the real stages complete. */
.scene-loading{
  position:absolute; inset:0; z-index:4;
  display:flex; align-items:center; justify-content:center;
  background:
    radial-gradient(120% 90% at 50% 20%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 70%),
    var(--surface-bg);
  opacity:1; transition:opacity .7s cubic-bezier(.22,1,.36,1), transform .7s cubic-bezier(.22,1,.36,1);
}
.scene-loading.is-done{opacity:0; transform:scale(1.03); pointer-events:none;}
.scene-loading-inner{
  display:flex; flex-direction:column; align-items:center; gap:9px;
  padding:24px; text-align:center; max-width:320px;
}
.scene-loading-mark{display:flex; gap:5px; margin-bottom:10px;}
.scene-loading-mark span{
  width:4px; height:22px; border-radius:2px; background:var(--accent);
  animation:scene-mark 1.25s cubic-bezier(.45,0,.55,1) infinite;
}
.scene-loading-mark span:nth-child(2){animation-delay:.14s; opacity:.72;}
.scene-loading-mark span:nth-child(3){animation-delay:.28s; opacity:.46;}
@keyframes scene-mark{
  0%,100%{transform:scaleY(.42);} 50%{transform:scaleY(1);}
}
.scene-loading-name{
  font-size:26px; font-weight:900; letter-spacing:.19em;
  color:var(--text); line-height:1;
}
.scene-loading-ar{font-size:14px; font-weight:800; color:var(--accent);}
.scene-loading-line{font-size:12.5px; color:var(--text-dim); margin-top:4px; line-height:1.5;}
.scene-loading-bar{
  width:180px; height:2px; border-radius:2px; margin-top:12px;
  background:color-mix(in srgb, var(--text-dim) 24%, transparent); overflow:hidden;
}
.scene-loading-bar i{
  display:block; height:100%; width:0; border-radius:2px; background:var(--accent);
  transition:width .5s cubic-bezier(.22,1,.36,1);
}
.scene-loading-stage{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:10px; letter-spacing:.09em; text-transform:uppercase;
  color:var(--text-dim); opacity:.75; min-height:12px;
}
@media (prefers-reduced-motion:reduce){
  .scene-loading-mark span{animation:none; transform:scaleY(.7);}
  .scene-loading{transition:opacity .2s linear;}
}

/* While the module is in flight the still drawing carries the screen. */
.wiz-landing.is-scene-loading .wiz-campus{opacity:.9;}

.wiz-campus-3d.can-turn{cursor:grab; touch-action:none;}
.wiz-campus-3d.is-turning{cursor:grabbing;}
/* Once the live scene is up the still drawing would show through the gaps in
   the line-work as a doubled campus, so it stands down. */
.wiz-campus-3d.is-live ~ .wiz-campus,
.wiz-landing:has(.wiz-campus-3d.is-live) .wiz-campus{opacity:0;}

/* A scrim between the campus and the words. With a still drawing the copy
   could sit straight on it; with the gate now standing at full height right
   behind the wordmark, "Every course in your degree" was running across an
   archway. This darkens a band through the middle and leaves the top and the
   bottom of the scene clear, so the drawing still reads at the edges and the
   text has a surface of its own. */
.wiz-landing-line,
.wiz-landing-note{
  /* Legibility bought locally. These two sit over line-work, and the
     alternative — pushing the scrim darker until they cleared — dimmed the
     whole campus to rescue two sentences. A halo in the card's own colour
     does it without touching anything else. */
  text-shadow:
    0 0 4px var(--surface-bg),
    0 0 9px var(--surface-bg),
    0 0 18px var(--surface-bg),
    0 0 26px var(--surface-bg);
}
/* The closing note sits over the busiest part of the drawing — the fountain
   and the steps — and it is the dimmest text on the screen. It keeps its
   halo and gets a little of its contrast back. */
.wiz-landing-note{color:var(--text-dim); opacity:1;}
.wiz-landing::after{
  content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
  background:linear-gradient(180deg,
    transparent 0%,
    color-mix(in srgb, var(--surface-bg) 26%, transparent) 30%,
    color-mix(in srgb, var(--surface-bg) 52%, transparent) 44%,
    color-mix(in srgb, var(--surface-bg) 52%, transparent) 64%,
    color-mix(in srgb, var(--surface-bg) 18%, transparent) 78%,
    transparent 100%);
}

/* A small language switch, before a plan exists to switch. */
.wiz-lang{
  position:absolute; top:2px; inset-inline-end:2px; z-index:2;
  display:inline-flex; align-items:center; justify-content:center;
  min-width:56px; padding:7px 13px; border-radius:999px;
  background:color-mix(in srgb, var(--accent) 12%, transparent);
  border:1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  color:var(--accent); font-family:inherit; font-size:12px; font-weight:800;
  cursor:pointer; transition:background .15s ease;
}
.wiz-lang:hover{background:color-mix(in srgb, var(--accent) 20%, transparent);}
.wiz-lang:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}

  .wiz-landing{position:relative;overflow:hidden;
    display:flex;flex-direction:column;align-items:center;text-align:center;
    padding:52px 30px 30px;gap:0;
    /* Desktop keeps the card at its own content height, which is short enough
       that a contained picture would shrink to a strip down the middle. This
       floor gives the drawing room without moving anything on it. */
    min-height:min(76vh, 700px);
  }
  .wiz-brand{position:relative; z-index:1;display:flex;flex-direction:column;align-items:center;gap:10px;}
  .wiz-brand-mark{
    width:74px;height:74px;object-fit:contain;border-radius:20px;
    box-shadow:0 10px 30px rgba(0,0,0,.35);
  }
  .wiz-brand-name{
    font-size:34px;font-weight:900;letter-spacing:.10em;color:var(--text);
    line-height:1;
  }
  /* The Arabic sits under the wordmark as a tagline, not beside it: at 34px
     the Latin name is the mark, and pairing them on one line makes neither
     read as the title. */
  .wiz-brand-ar{
    font-size:19px;font-weight:700;color:var(--accent);line-height:1;
    letter-spacing:.02em;
  }
  .wiz-landing-line{position:relative; z-index:1;
    margin:22px 0 0;font-size:13.5px;line-height:1.65;color:var(--text-dim);
    max-width:270px;
  }
  .wiz-landing-actions{position:relative; z-index:1;
    display:flex;flex-direction:column;align-items:stretch;gap:10px;
    width:100%;max-width:280px;margin-top:30px;
  }
  .wiz-landing-actions .wiz-continue{width:100%;padding:14px 22px;font-size:14px;}
  .wiz-skip{
    background:none;border:1px solid var(--line);border-radius:999px;
    color:var(--text-dim);font-size:13px;font-weight:700;font-family:inherit;
    padding:12px 22px;cursor:pointer;
  }
  .wiz-skip:hover{color:var(--text);border-color:var(--text-dim);}
  .wiz-landing-note{position:relative; z-index:1;
    margin:26px 0 0;font-size:11px;line-height:1.6;color:var(--text-dim);
    opacity:.7;max-width:280px;
  }

  /* The sign-in panel: one form, one mode switch. */
  .wiz-fields{display:flex;flex-direction:column;gap:9px;width:100%;}
  .wiz-fields input{
    width:100%;background:var(--bg);border:1px solid var(--line);border-radius:12px;
    padding:12px 14px;font-size:13.5px;font-family:inherit;color:var(--text);
  }
  .wiz-fields input:focus{outline:2px solid var(--accent);outline-offset:1px;}
  .wiz-switch{
    background:none;border:none;color:var(--accent);font-size:12px;font-weight:700;
    font-family:inherit;cursor:pointer;padding:10px 4px 0;
  }

  .wiz-skip-link{
    position:absolute;top:14px;right:16px;background:none;border:none;
    color:var(--text-dim);font-size:11.5px;font-weight:700;cursor:pointer;
    font-family:inherit;padding:4px;
  }
  .wiz-skip-link:hover{color:var(--text);}

  .wiz-body{
    display:flex;flex-direction:column;align-items:center;text-align:center;
    padding:34px 26px 22px;gap:6px;
  }
  .wiz-ic{font-size:34px;line-height:1;margin-bottom:4px;}
  .wiz-mascot{width:64px;height:64px;object-fit:contain;border-radius:14px;}
  .wiz-title{font-size:16px;font-weight:800;color:var(--text);}
  .wiz-sub{font-size:12px;color:var(--text-dim);line-height:1.6;max-width:320px;}
  .wiz-content{width:100%;text-align:left;margin-top:14px;}
  .plan-page.rtl-mode .wiz-content{text-align:right;}

  .wiz-foot{
    display:flex;justify-content:space-between;align-items:center;
    padding:14px 20px;border-top:1px solid var(--line);
  }
  .wiz-back{
    background:none;border:none;color:var(--text-dim);font-size:12px;font-weight:700;
    cursor:pointer;font-family:inherit;padding:4px;
  }
  .wiz-back:hover{color:var(--text);}
  .wiz-continue{
    background:var(--accent);color:var(--on-accent);border:none;border-radius:999px;
    padding:10px 22px;font-weight:800;font-size:12.5px;font-family:inherit;cursor:pointer;
  }

  /* ==========================================================================
     14c. STORY STACK — the shared swipeable card sequence (js/56-story-
     stack.js) behind both the semester recap and the prerequisite "why is
     this locked?" walkthrough. Segmented top progress bar (Instagram-
     Stories style) instead of the wizard's single continuous fill, since
     these are N ad-hoc cards rather than a fixed set of onboarding steps.
     ========================================================================== */

  /* Higher than the base .modal-overlay's z-index:1000 — the story stack can
     legitimately open ON TOP OF another already-open modal (the prereq
     graph opens "why is this locked?" from inside itself, on purpose), and
     with two overlays tied on z-index the one later in the DOM would win
     regardless of which one the student actually means to interact with. */
  #storyStackOverlay{z-index:1100;}

  .story-stack-card{max-width:420px;padding:0;overflow:hidden;}

  .story-progress{display:flex;gap:4px;padding:12px 14px 0;}
  .story-progress span{flex:1;height:3px;border-radius:2px;background:rgba(255,255,255,.14);}
  .story-progress span.filled{background:var(--accent);}

  .story-close{
    position:absolute;top:8px;right:10px;background:none;border:none;
    color:var(--text-dim);font-size:22px;cursor:pointer;line-height:1;padding:6px;
  }
  .story-close:hover{color:var(--text);}

  .story-crumb{padding:10px 30px 0;font-size:10.5px;color:var(--text-dim);text-align:center;}

  .story-body{
    position:relative;display:flex;flex-direction:column;align-items:center;text-align:center;
    padding:26px 24px 18px;gap:8px;min-height:170px;justify-content:center;touch-action:pan-y;
  }
  /* Holds an inline SVG from js/04-icons.js. font-size is kept for the one
     remaining caller that still passes a character. */
  .story-ic{
    width:64px;height:64px;border-radius:18px;background:rgba(255,255,255,.06);
    display:flex;align-items:center;justify-content:center;font-size:30px;line-height:1;
    color:var(--accent);
  }
  .story-ic svg{width:34px;height:34px;}
  .story-title{font-size:17px;font-weight:800;color:var(--text);}
  .story-sub{font-size:12px;color:var(--text-dim);line-height:1.6;max-width:320px;}
  .story-content{width:100%;margin-top:8px;}

  /* Status pill under a story card's title — colored per status so a card
     reads at a glance without needing the icon above it to double as a
     status indicator too (the icon is the course's subject instead). */
  .story-badge{
    display:inline-block;font-size:10px;font-weight:800;letter-spacing:.03em;text-transform:uppercase;
    border-radius:999px;padding:4px 12px;
  }
  .story-badge-locked{background:rgba(255,255,255,.06);color:var(--text-dim);border:1px solid var(--line);}
  .story-badge-unlocked{background:color-mix(in srgb, var(--accent) 14%, transparent);color:var(--accent);border:1px solid var(--accent);}
  .story-badge-passed{background:rgba(47,191,113,.14);color:#8ff0bd;border:1px solid var(--unlock);}

  .story-foot{
    display:flex;justify-content:space-between;align-items:center;
    padding:14px 20px;border-top:1px solid var(--line);
  }
  .story-stack-card[dir="rtl"] .story-foot{flex-direction:row-reverse;}
  .story-back{
    background:none;border:none;color:var(--text-dim);font-size:12px;font-weight:700;
    cursor:pointer;font-family:inherit;padding:4px;
  }
  .story-back:hover{color:var(--text);}
  .story-continue{
    background:var(--accent);color:var(--on-accent);border:none;border-radius:999px;
    padding:10px 22px;font-weight:800;font-size:12.5px;font-family:inherit;cursor:pointer;
  }

  /* Semester-recap-specific card content: a chip list of finished courses
     and a big GPA reveal number — generic enough to belong here rather than
     a whole separate stylesheet section for one feature's two card types. */
  .story-course-chips{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;}
  .story-course-chip{
    font-size:11px;font-weight:700;background:rgba(255,255,255,.05);border:1px solid var(--line);
    border-radius:999px;padding:5px 12px;
  }
  .story-gpa-big{font-size:40px;font-weight:800;color:var(--accent);line-height:1;margin-top:4px;}
  .story-cap{
    font-size:10.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
    color:var(--text-dim);margin-top:6px;
  }

  /* 59 · The cumulative before and after the semester that just closed. The
     "was" is deliberately quieter than the "now" — the point of the card is
     where you have arrived, and showing both at equal weight makes the
     reader do the comparison the card exists to have already made. */
  .story-move{
    display:flex;align-items:baseline;justify-content:center;gap:12px;
    font-variant-numeric:tabular-nums;margin-top:4px;
  }
  .story-move-was{font-size:24px;font-weight:700;color:var(--text-dim);}
  .story-move-arrow{font-size:20px;color:var(--text-dim);}
  .story-move-now{font-size:40px;font-weight:800;line-height:1;}
  .story-move-up .story-move-now{color:var(--skills);}
  .story-move-down .story-move-now{color:var(--warn);}
  .story-move-flat .story-move-now{color:var(--accent);}
  [dir="rtl"] .story-move{flex-direction:row-reverse;}

  /* Prerequisite "why is this locked?" card content — one chip per direct
     prerequisite of the course the current card is about, so a branch (two
     or more prereqs) doesn't get lost: done ones read as already-solved,
     the one actually being followed next is highlighted. */
  .story-why-note{font-size:12px;color:var(--text-dim);margin:0 0 6px;}
  .story-chip-row{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;}
  .story-chip{
    font-size:11px;font-weight:700;border-radius:999px;padding:5px 12px;
    border:1.5px solid var(--line);background:rgba(255,255,255,.04);
  }
  .story-chip-done{border-color:var(--unlock);color:#8ff0bd;}
  .story-chip-focus{border-color:var(--accent);color:var(--accent);}

  /* ==========================================================================
     15. DEVELOPER MODE & PLAN EDITOR — everything gated behind the hidden
     developer login: the panel itself, imported-plan cards and their
     simplified viewer, and the drag-and-drop semester editor
     ========================================================================== */

  .dev-modal-card{max-width:420px;}

  /* Course Library row list (js/28-imported.js openLibrary/renderList) —
     was inline-styled per row; pulled into classes so a phone-only override
     further down can actually win against it (an inline style attribute
     beats any stylesheet rule short of !important). Values here reproduce
     the old inline styles exactly, so desktop is pixel-identical. */
  .lib-row{font-size:12.5px;padding:6px 0;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:8px;}
  .lib-row-meta{color:var(--text-dim);}
  .lib-list{max-height:300px;overflow-y:auto;}
  .lib-add-btn{padding:3px 10px;font-size:11px;}

  /* Shelves. A plan's courses are grouped by the requirement each satisfies,
     so the list is scanned by section rather than read end to end. The
     header is sticky inside the scrolling list — on a 130-course plan the
     shelf you are reading scrolls out of view long before its last row
     does, and then every row looks the same again. */
  .lib-shelf + .lib-shelf{margin-top:10px;}
  /* A button now: every shelf open at once put 130 rows on screen the moment
     the Library opened, which is the wall the shelves were meant to break
     up. Closed until asked for. */
  .lib-shelf-head{
    position:sticky; top:0; z-index:1;
    display:flex; align-items:center; gap:10px; width:100%;
    padding:9px 2px; margin-bottom:2px;
    background:var(--panel); border:none; border-bottom:1px solid var(--accent);
    font-family:inherit; cursor:pointer; text-align:start;
  }
  .lib-shelf-chev{
    flex:none; font-size:16px; line-height:1; color:var(--accent);
    transition:transform .18s ease;
  }
  .lib-shelf-open .lib-shelf-chev{transform:rotate(90deg);}
  .lib-shelf-count{margin-inline-start:auto;}
  .lib-shelf-body{display:none;}
  .lib-shelf-open .lib-shelf-body{display:block; padding-top:4px;}
  @media (prefers-reduced-motion:reduce){ .lib-shelf-chev{transition:none;} }
  .lib-shelf-label{
    font-size:10.5px; font-weight:800; letter-spacing:.06em;
    text-transform:uppercase; color:var(--accent);
  }
  .lib-shelf-count{
    font-size:10.5px; font-weight:700; color:var(--text-dim);
    font-variant-numeric:tabular-nums; white-space:nowrap;
  }
  .lib-row-name{min-width:0;}

  /* Settings modal's top tab bar — four groups (Account/Preferences/Data/
     Help) instead of one long scroll of every setting stacked together.
     Scrolls horizontally rather than wrapping on the narrowest phones: a
     wrapped second row of tabs reads as a second bar, not an overflow of
     the first. */
  .settings-tabbar{
    display:flex; gap:4px; background:rgba(255,255,255,.04); border-radius:11px;
    padding:4px; margin:14px 0 20px; overflow-x:auto;
    scrollbar-width:none; -ms-overflow-style:none; /* Firefox / old Edge: swipe still works, no visible track */
  }
  .settings-tabbar::-webkit-scrollbar{display:none;} /* Chrome/Safari: same */
  .settings-tab{
    flex:1; display:flex; align-items:center; justify-content:center; gap:6px;
    text-align:center; padding:8px 10px; border-radius:8px; font-size:12.5px; font-weight:700;
    color:var(--text-dim); cursor:pointer; white-space:nowrap;
  }
  .settings-tab.active{background:var(--accent); color:var(--on-accent);}
  .settings-tab-icon{font-size:14px;}

  /* ---- theme picker (Settings > Preferences) ---- */
  .theme-picker-grid{
    display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px;
    margin-bottom:6px;
  }
  .theme-swatch{
    display:flex; flex-direction:column; gap:8px; align-items:stretch;
    padding:8px; border-radius:12px; border:1.5px solid var(--line);
    background:rgba(255,255,255,.03); cursor:pointer; font-family:inherit;
    transition:border-color .15s ease, transform .12s ease;
  }
  .theme-swatch:hover{transform:translateY(-1px); border-color:var(--accent);}
  .theme-swatch-active{border-color:var(--accent); box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent);}
  .theme-swatch-preview{
    height:34px; border-radius:8px; border:1px solid rgba(0,0,0,.15);
    background:linear-gradient(135deg, var(--sw-bg) 0%, var(--sw-bg) 52%, var(--sw-accent) 52%, var(--sw-accent) 100%);
  }
  .theme-swatch-label{
    display:flex; align-items:center; gap:6px;
    font-size:11.5px; font-weight:700; color:var(--text);
  }
  .theme-swatch-check{margin-left:auto; opacity:0; color:var(--accent); font-weight:800;}
  .theme-swatch-active .theme-swatch-check{opacity:1;}
  html[dir="rtl"] .theme-swatch-check{margin-left:0; margin-right:auto;}

  /* ---- my colours (js/61-theme-custom.js) ----
     The Customize button under the theme grid, and the panel it opens: a
     real hue/saturation wheel on a canvas, a lightness slider, a hex box and
     the system colour picker, all editing whichever of the two colours is
     selected above them. */
  .tc-open-row{display:flex;margin:2px 0 4px;}
  .tc-panel{
    border:1px solid var(--line);border-radius:12px;padding:12px;
    margin-top:8px;background:var(--surface-bg);
  }
  .tc-lead{margin-top:0;}
  .tc-targets{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;}
  .tc-target{
    flex:1 1 130px;display:flex;align-items:center;gap:9px;padding:8px;
    border-radius:10px;border:1.5px solid var(--line);background:none;
    cursor:pointer;font-family:inherit;text-align:start;
  }
  .tc-target-on{border-color:var(--accent);}
  .tc-target-chip{
    width:26px;height:26px;flex:0 0 26px;border-radius:50%;
    border:1px solid rgba(0,0,0,.25);
  }
  .tc-target-txt{display:flex;flex-direction:column;gap:1px;font-size:11.5px;font-weight:700;color:var(--text);}
  .tc-target-txt code{font-size:10.5px;color:var(--text-dim);font-weight:600;}
  .tc-editor{display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap;}
  .tc-wheel{border-radius:50%;cursor:crosshair;touch-action:none;flex:0 0 auto;}
  .tc-fields{flex:1 1 190px;min-width:180px;display:flex;flex-direction:column;gap:6px;}
  .tc-lab{font-size:10.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--text-dim);}
  .tc-light{
    appearance:none;-webkit-appearance:none;width:100%;height:12px;
    border-radius:999px;border:1px solid var(--line);cursor:pointer;
    background:var(--tc-hue-bg,linear-gradient(to right,#000,#888,#fff));
  }
  .tc-light::-webkit-slider-thumb{
    -webkit-appearance:none;width:18px;height:18px;border-radius:50%;
    background:#fff;border:2px solid var(--accent);cursor:pointer;
  }
  .tc-light::-moz-range-thumb{
    width:18px;height:18px;border-radius:50%;
    background:#fff;border:2px solid var(--accent);cursor:pointer;
  }
  .tc-hex-row{display:flex;gap:8px;align-items:center;}
  .tc-hex{
    flex:1;min-width:0;font-family:ui-monospace,Menlo,Consolas,monospace;
    font-size:13px;padding:8px 10px;border-radius:8px;
    border:1px solid var(--line);background:var(--search-bg);color:var(--text);
  }
  .tc-native{
    width:44px;height:38px;padding:2px;flex:0 0 44px;
    border:1px solid var(--line);border-radius:8px;background:none;cursor:pointer;
  }
  .tc-msg{margin:2px 0 0;font-size:11px;color:var(--text-dim);}
  .tc-msg-warn{color:var(--prereq);font-weight:700;}
  /* The preview paints the DERIVED values, not the two chosen ones — the
     point is to show what the app will look like, and seven of the nine
     colours in it were worked out rather than picked. */
  .tc-preview{
    display:flex;align-items:center;gap:10px;margin-top:12px;padding:12px;
    border:1px solid var(--line);border-radius:10px;flex-wrap:wrap;
  }
  .tc-prev-card{
    display:flex;align-items:center;gap:8px;padding:8px 12px;
    border:1px solid;border-radius:8px;font-size:12px;font-weight:700;
  }
  .tc-prev-btn{padding:7px 14px;border-radius:999px;font-size:12px;font-weight:800;}

  .tc-presets{margin-top:14px;}
  .tc-presets-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);margin-bottom:8px;}
  .tc-presets-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;}
  .tc-preset-wrap{position:relative;}
  .tc-preset-pick{
    width:34px;height:34px;border-radius:50%;padding:0;border:2px solid var(--line);
    background:none;cursor:pointer;overflow:hidden;
  }
  .tc-preset-active .tc-preset-pick{border-color:var(--accent);}
  .tc-preset-dot{display:block;width:100%;height:100%;border-radius:50%;position:relative;}
  .tc-preset-dot i{
    position:absolute;right:0;bottom:0;width:60%;height:60%;border-radius:50%;
    border:2px solid var(--panel);font-style:normal;
  }
  .tc-preset-del{
    position:absolute;top:-5px;right:-5px;width:16px;height:16px;border-radius:50%;
    background:var(--prereq);color:#fff;border:none;font-size:11px;line-height:1;cursor:pointer;
    display:none;align-items:center;justify-content:center;padding:0;
  }
  .tc-preset-wrap:hover .tc-preset-del,
  .tc-preset-wrap:focus-within .tc-preset-del{display:flex;}
  .tc-preset-add{
    padding:6px 12px;height:34px;border-radius:999px;border:1.5px dashed var(--line);background:none;
    color:var(--text-dim);font-size:11.5px;font-weight:700;cursor:pointer;white-space:nowrap;font-family:inherit;
  }
  .tc-preset-add:hover{border-color:var(--accent);color:var(--text);}
  .tc-preset-name-row{display:flex;gap:8px;margin-top:10px;}
  .tc-preset-name-row input{
    flex:1;padding:7px 10px;border-radius:8px;border:1px solid var(--line);
    background:var(--search-bg);color:var(--text);font-family:inherit;font-size:12.5px;
  }
  .tc-actions{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;}

  .dev-wrong-pass{color:var(--prereq);font-size:12.5px;font-weight:700;margin-top:8px;}

  .dev-panel-section{border-top:1px solid var(--line);margin-top:16px;padding-top:16px;}

  .dev-panel-section h3{margin:0 0 10px;font-size:13px;color:var(--text);}

  .imported-plan-row{
    display:flex;justify-content:space-between;align-items:center;gap:10px;
    background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:8px;
    padding:9px 12px;margin-bottom:8px;font-size:12.5px;
  }

  .imported-plan-row .ipr-name{font-weight:700;color:var(--text);}

  .imported-plan-row .ipr-meta{font-size:10.5px;color:var(--text-dim);}

  .dev-warn-list{font-size:11.5px;color:var(--connect);margin:8px 0 0;padding-left:18px;}

  .dev-success-msg{color:var(--unlock);font-size:12.5px;font-weight:700;margin-top:8px;}

  .dev-error-msg{color:var(--prereq);font-size:12.5px;font-weight:700;margin-top:8px;}
  /* ================= IMPORTED PLAN (simplified renderer) ================= */

  .imported-plan-card{
    background:var(--panel);border:1px dashed var(--accent);border-radius:12px;
    padding:18px 20px;cursor:pointer;transition:transform .15s ease;
  }

  .imported-plan-card:hover{transform:translateY(-3px);}

  .imp-semester-block{margin-bottom:22px;}

  .imp-semester-title{font-size:12px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px;}

  .imp-course-grid{display:flex;flex-wrap:wrap;gap:10px;}

  .imp-course-card{
    background:var(--core);color:var(--core-tx);border-radius:8px;padding:10px 12px;
    min-width:150px;flex:1 1 170px;position:relative;border:2px solid transparent;
  }

  .imp-course-card.completed{opacity:.55;}

  .imp-course-card.locked{opacity:.5;}

  .imp-course-card .imp-name{font-weight:700;font-size:13px;margin-bottom:4px;}

  .imp-course-card .imp-lock-note{font-size:10px;opacity:.85;}

  .imp-course-card .imp-cr{font-size:10px;opacity:.85;}

  /* ---- structured renderer (registered as a real plan; reuses .course/
     .course-row so GPA, Degree Audit, Achievements and the drag-and-drop
     Plan Editor all work on it exactly like a built-in major) ---- */
  .imp-year-block{
    background:var(--panel);border:1px solid var(--line);border-radius:12px;
    padding:18px 20px;margin-bottom:16px;
  }
  .imp-year-header{
    display:flex;align-items:center;justify-content:space-between;gap:10px;
    margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--line);
  }
  /* A folded year is only its header, so the bottom border and the gap
     under it would draw a line with nothing beneath it. */
  .imp-year-block.year-collapsed{padding-bottom:14px;}
  .imp-year-block.year-collapsed > .imp-year-header{
    margin-bottom:0;padding-bottom:0;border-bottom:0;
  }
  .imp-year-header h3{margin:0;font-size:15px;color:var(--text);}
  .imp-year-actions{display:flex;gap:8px;flex-wrap:wrap;}

  /* The whole header row is the toggle. It is one <button> with the
     chevron, the title and the counts inside it, so the tap target is the
     full width of the card rather than a chevron a thumb has to find —
     and the edit-mode year actions stay OUTSIDE it, because a button
     inside a button is invalid and stops firing. */
  .imp-year-toggle{
    flex:1 1 auto;min-width:0;
    display:flex;align-items:center;gap:10px;
    background:none;border:0;padding:4px 0;margin:-4px 0;
    font-family:inherit;color:inherit;cursor:pointer;text-align:start;
    -webkit-tap-highlight-color:transparent;
  }
  .imp-year-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:8px;}
  .iy-chev{
    display:inline-flex;align-items:center;justify-content:center;
    width:22px;height:22px;flex:0 0 22px;color:var(--text-dim);
  }
  .imp-year-toggle:hover .iy-chev{color:var(--accent);}
  /* How far through the year, as a shape, at the far end of the row: a
     folded plan reads as a column of rings without opening anything. The
     fraction it replaces is on the ring's aria-label, not lost. */
  .iy-ring{
    margin-inline-start:auto;flex:0 0 auto;
    display:inline-flex;align-items:center;line-height:0;
  }
  .iy-ring .iy-ring-bg{
    fill:none;stroke:var(--line);stroke-width:3;
  }
  .iy-ring .iy-ring-fill{
    fill:none;stroke:var(--accent);stroke-width:3;stroke-linecap:round;
  }
  .iy-hours{
    flex:0 0 auto;font-size:11px;font-weight:800;color:var(--accent);
    font-variant-numeric:tabular-nums;
  }
  /* The only status a card still prints is the closed one, and it names the
     course it is waiting for rather than the word "closed". Dim, because it
     is the answer to a question you only ask about a card you cannot use. */
  .course-meta .cm-locked{
    color:var(--text-dim);font-style:italic;
    overflow:hidden;text-overflow:ellipsis;
  }
  /* "term suggested" — the app put this course in this semester, not the
     published plan (js/28-imported.js). Warn-coloured, like the heavy-term
     note: something to know, not something wrong. */
  .course-meta .cm-suggested{
    color:var(--warn);font-style:italic;flex:0 0 auto;
  }

  /* 52 · "which level?" on the three English cards, until the placement is
     answered (js/77-english-level.js). It is the one chip on a card that
     asks for something rather than reporting something, so it is the accent
     rather than the dim or the warn — and it disappears the moment it is
     answered, which is why it can afford to be loud. */
  .course-meta .cm-eng-ask{
    color:var(--accent);font-weight:800;flex:0 0 auto;
  }

  /* Following a friend's snapshot (js/82-follow.js). Two bars, same scale,
     and what each of you has that the other does not. */
  .fl-row{margin:0 0 10px;}
  .fl-line{display:flex;align-items:baseline;gap:10px;font-size:12.5px;}
  .fl-name{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .fl-num{
    margin-inline-start:auto;color:var(--text-dim);
    font-variant-numeric:tabular-nums;
  }
  .fl-track{
    height:7px;margin-top:5px;border-radius:99px;
    background:var(--line);overflow:hidden;
  }
  .fl-track i{display:block;height:100%;border-radius:99px;background:var(--text-dim);}
  .fl-me .fl-track i{background:var(--accent);}
  .fl-me .fl-name{color:var(--accent);}
  .fl-note{
    margin:-4px 0 12px;font-size:11.5px;color:var(--text-dim);
    overflow:hidden;text-overflow:ellipsis;
  }
  .fl-x{
    background:none;border:0;padding:0 2px;cursor:pointer;
    font-family:inherit;font-size:11.5px;color:var(--text-dim);
    text-decoration:underline;
  }
  .fl-x:hover{color:var(--prereq);}

  /* "What do I need on the final", answered before the list of every grade
     tier rather than inside it (js/21-course-modal-extras.js). */
  .as-need{
    display:grid;gap:4px;margin:0 0 8px;padding:9px 11px;
    border:1px solid var(--line);border-radius:9px;
    background:rgba(255,255,255,.03);
  }
  .as-need-row{
    display:flex;align-items:baseline;gap:10px;
    font-size:12.5px;color:var(--text-dim);
  }
  .as-need-row b{
    margin-inline-start:auto;font-size:15px;font-weight:800;color:var(--text);
    font-variant-numeric:tabular-nums;
  }
  .as-need-row b.as-ok{color:var(--unlock);font-size:13px;}

  /* Two cards, one course: the attempt that was replaced and the one that
     counts. The replaced card keeps its grade — it is still on the
     transcript and still in the semester GPA — but reads as spent. */
  .course.course-superseded{opacity:.62;}
  .course-meta .cm-attempt{flex:0 0 auto;font-weight:800;}
  .course-meta .cm-replaced{color:var(--text-dim);text-decoration:line-through;}
  .course-meta .cm-counts{color:var(--unlock);}

  /* An elective shelf measures the hours the plan asks for, not the size of
     the shelf (js/28-imported.js poolMeterHtml). */
  .imp-pool-meter{
    display:flex;align-items:center;gap:10px;
    margin:0 0 9px;
  }
  /* .pw-track is only styled inside .progress-widget, so the shelf meter
     carries its own bar rather than inheriting an invisible one. */
  .imp-pool-meter .pw-track{
    flex:1 1 auto;min-width:60px;height:6px;border-radius:99px;
    background:var(--line);overflow:hidden;
  }
  .imp-pool-meter .pw-fill{
    height:100%;border-radius:99px;background:var(--accent);
  }
  .imp-pool-num{
    flex:0 0 auto;font-size:11.5px;color:var(--text-dim);
    font-variant-numeric:tabular-nums;white-space:nowrap;
  }
  .imp-pool-num b{color:var(--text);font-weight:800;}

  /* Next semester, suggested, under the marked one (js/78-you-are-here.js).
     Plan My Next Semester used to be a trip; this is the same ranking,
     where the question is asked. */
  .ynx{
    margin:0 0 18px;padding:12px 14px;
    border:1px dashed var(--line);border-radius:10px;
    background:rgba(255,255,255,.02);
  }
  .ynx-head{display:flex;align-items:baseline;gap:10px;margin-bottom:9px;}
  .ynx-label{
    font-size:10.5px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;
    color:var(--text-dim);
  }
  .ynx-hours{
    margin-inline-start:auto;font-size:11.5px;font-weight:800;color:var(--accent);
    font-variant-numeric:tabular-nums;
  }
  .ynx-list{display:flex;flex-wrap:wrap;gap:7px;}
  .ynx-card{
    flex:1 1 190px;min-width:0;text-align:start;cursor:pointer;
    display:flex;flex-direction:column;gap:2px;
    padding:8px 10px;border:1px solid var(--line);border-radius:8px;
    background:var(--panel);font-family:inherit;color:var(--text);
  }
  .ynx-card:hover{border-color:var(--accent);}
  .ynx-card:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .ynx-name{font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .ynx-meta{font-size:10.5px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .ynx-empty{margin:0;font-size:12px;color:var(--text-dim);}
  .ynx-more{
    margin-top:9px;background:none;border:0;padding:0;cursor:pointer;
    font-family:inherit;font-size:12px;font-weight:700;color:var(--accent);
  }
  .ynx-more:hover{text-decoration:underline;}

  /* One row asks which kind of change (js/37-sidebar.js openPlanChooser). */
  .cp-choice{display:grid;gap:9px;margin-top:6px;}
  .cp-choice-btn{
    display:flex;align-items:center;gap:12px;text-align:start;cursor:pointer;
    padding:13px 14px;border:1px solid var(--line);border-radius:11px;
    background:var(--panel);font-family:inherit;color:var(--text);
  }
  .cp-choice-btn:hover{border-color:var(--accent);}
  .cp-choice-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .cp-choice-icon{
    flex:none;display:inline-flex;align-items:center;justify-content:center;
    width:38px;height:38px;border-radius:10px;color:var(--accent);
    background:color-mix(in srgb, var(--accent) 14%, transparent);
  }
  .cp-choice-text{display:flex;flex-direction:column;gap:2px;min-width:0;}
  .cp-choice-text b{font-size:13.5px;font-weight:700;}
  .cp-choice-text span{font-size:11.5px;color:var(--text-dim);}

  /* Held down: the year says so, so the dimming reads as deliberate rather
     than as the page having gone wrong. */
  .imp-year-block.year-peek{box-shadow:0 0 0 2px var(--accent) inset;}
  .imp-add-course-card{
    background:rgba(255,255,255,.03);border:2px dashed var(--line);border-radius:8px;
    min-width:150px;flex:1 1 170px;display:flex;align-items:center;justify-content:center;
    cursor:pointer;color:var(--text-dim);font-size:24px;font-weight:300;min-height:56px;
    transition:border-color .15s ease,color .15s ease;
  }
  .imp-add-course-card:hover{border-color:var(--accent);color:var(--accent);}
  .imp-card-btn-row{
    /* inset-inline-end, not right: in Arabic these stayed pinned to the
       right edge while the title's clearance moved to the left, so the pen
       and the close button sat on top of the course name. */
    position:absolute; top:4px; inset-inline-end:4px; z-index:4;
    display:flex; gap:3px;
  }
  .imp-card-btn-row button{
    width:18px; height:18px; border-radius:50%; border:none;
    background:rgba(0,0,0,.35); color:#fff; font-size:10px; line-height:1;
    cursor:pointer; display:flex; align-items:center; justify-content:center;
    padding:0;
  }
  .imp-remove-course-btn:hover{background:var(--prereq);}
  .imp-edit-course-btn:hover{background:var(--accent);}
  .imp-structure-actions{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0;}

  .course-cr{font-size:10px;opacity:.8;margin-top:2px;}

  /* Year, whether it is open to this student, and the hours — the three
     things checked on every card. The registration code used to sit under
     them on a fourth line; it is read once, at registration, and is in the
     course details popup along with everything else needed then. */
  .course-meta{
    font-size:calc(10px * var(--card-scale, 1));
    margin-top:3px;line-height:1.35;font-weight:600;
    /* Was a hardcoded near-white, from when a card was a block of colour with
       white ink on it. The card is a panel now, at every width, so the meta
       line takes the theme's dim ink like every other secondary line. */
    color:var(--text-dim);
    /* One line, always. The line can now carry a prerequisite's full name on
       a locked card, and left to wrap it made those cards two rows taller
       than the ones beside them. */
    display:flex;align-items:baseline;min-width:0;white-space:nowrap;
    /* clear of the tick in the corner */
    padding-inline-end:18px;
  }
  .course-meta .cm-locked{flex:0 1 auto;min-width:0;}
  .course-meta .cm-hours,
  .course-meta .cm-sep{flex:0 0 auto;}
  /* The separator's own spaces are trimmed away by flex, so the gap is a
     margin rather than the " · " it is written as. */
  .course-meta .cm-sep{margin:0 4px;}

  /* The card is a real button now, so it needs a real focus ring. */
  .course:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  /* Swipe right to mark passed, left to undo (js/57-card-input.js). The card
     follows the finger and colours the edge it is heading toward, so the
     gesture explains itself the first time it is tried by accident. */
  .course.swipe-pass{box-shadow:-6px 0 0 0 var(--unlock);}
  .course.swipe-undo{box-shadow:6px 0 0 0 var(--prereq);}

  /* Reading-size picker (Settings -> Preferences), same shape as the theme
     swatches beside it so the two read as one set of appearance controls. */
  .size-picker{display:flex;gap:8px;flex-wrap:wrap;}
  .size-pick{
    display:flex;flex-direction:column;align-items:center;gap:3px;
    min-width:76px;padding:9px 12px;cursor:pointer;
    background:var(--panel);border:1px solid var(--line);border-radius:9px;
    color:var(--text-dim);font-size:11px;font-weight:700;font-family:inherit;
  }
  .size-pick:hover{border-color:var(--accent);color:var(--text);}
  .size-pick-active{border-color:var(--accent);color:var(--text);box-shadow:0 0 0 1px var(--accent) inset;}
  .size-pick:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  /* "Do this next" — the single recommendation above the dashboard's ranked
     list. Deliberately the widest, quietest thing on the screen: one label,
     one name, one sentence of reason. */
  .wn-lead{
    display:block;width:100%;text-align:left;cursor:pointer;font-family:inherit;
    background:var(--panel);border:1px solid var(--accent);border-radius:12px;
    padding:14px 16px;margin:0 0 14px;color:var(--text);
  }
  .wn-lead:hover{background:var(--header-bg);}
  .wn-lead:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .plan-page.rtl-mode .wn-lead, .sheet.rtl-mode .wn-lead{text-align:right;}
  .wn-lead-label{
    display:block;font-size:10.5px;font-weight:800;letter-spacing:.08em;
    text-transform:uppercase;color:var(--text-dim);margin-bottom:4px;
  }
  .wn-lead-name{display:block;font-size:17px;font-weight:800;line-height:1.25;}
  .wn-lead-cr{font-size:12px;font-weight:700;color:var(--accent);}
  .wn-lead-why{display:block;font-size:12.5px;color:var(--text-dim);margin-top:5px;line-height:1.5;}
  .wn-lead-cta{display:inline-block;margin-top:8px;font-size:12px;font-weight:800;color:var(--accent);}
  /* The way out of the Dashboard's one-line answer and onto the plan, where
     the full ranked list lives (js/50-whats-next.js, 'lead' mode). */
  .wn-on-plan{margin-top:12px; width:100%; justify-content:center;}

  /* ---------- Back bar (js/60-backbar.js) ----------
     Phones already turn a dialog into a page with a back arrow; this is the
     same affordance on a desktop, where the only exit used to be a small ✕
     in the corner. Students asked for it in those words: a button that says
     "back", because hunting for the way out is not a puzzle worth setting.

     The first version was a bordered pill on a panel background — the same
     colour as the card it sat on — parked above the dialog's heading with a
     divider under it. It read as a stray grey box rather than a control, and
     the student who asked for the feature said so. This version is a header
     row instead: a round arrow that looks pressable, the word next to it, and
     no divider competing with the heading right below. The right-hand padding
     keeps it clear of the ✕ that still sits in the corner. */
  /* Not margin:-6px anymore — .back-bar is the first thing inside
     .modal-card > .modal-body, which scrolls its own content (added later,
     to cap a tall dialog's height). A scroll container clips a child's
     negative top margin instead of letting it poke out above, which cut
     the top off the back arrow's circle. Zero margin here can never clip. */
  .back-bar{
    display:flex;align-items:center;gap:10px;
    margin:0 0 14px;padding-inline-end:30px;
    min-height:32px;
  }
  .back-bar-rtl{flex-direction:row-reverse;padding-inline-end:0;padding-inline-start:30px;}
  .back-bar-btn{
    display:inline-flex;align-items:center;gap:9px;cursor:pointer;
    font-family:inherit;font-size:13.5px;font-weight:800;letter-spacing:.01em;
    color:var(--text-dim);background:none;border:0;padding:0;
    border-radius:999px;
  }
  .back-bar-arrow{
    display:inline-flex;align-items:center;justify-content:center;
    width:32px;height:32px;flex:0 0 32px;border-radius:50%;
    background:var(--search-bg);border:1px solid var(--line);
    color:var(--text);line-height:1;
    transition:background .15s ease,border-color .15s ease,color .15s ease,transform .15s ease;
  }
  /* One drawn arrow, mirrored for Arabic, rather than two characters: the
     ← and → glyphs came from the body font and sat a pixel or two off
     centre in the circle, which is what made the bar look unfinished. */
  .back-bar-rtl .back-bar-arrow svg{transform:scaleX(-1);}
  /* --panel is the card's own background and --accent is picked to stand out
     against it, so accent-on-panel is readable in all six themes without any
     theme having to declare a seventh colour for this one button. */
  .back-bar-btn:hover{color:var(--text);}
  .back-bar-btn:hover .back-bar-arrow{
    background:var(--accent);border-color:var(--accent);color:var(--on-accent);
    transform:translateX(-2px);
  }
  .back-bar-rtl .back-bar-btn:hover .back-bar-arrow{transform:translateX(2px);}
  .back-bar-rtl .back-bar-btn:hover .back-bar-arrow{transform:translateX(2px);}
  .back-bar-btn:active .back-bar-arrow{transform:none;}
  .back-bar-btn:focus-visible{outline:none;}
  .back-bar-btn:focus-visible .back-bar-arrow{outline:2px solid var(--accent);outline-offset:2px;}
  .back-bar-title{
    font-size:12px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
    color:var(--text-dim);
  }
  @media (prefers-reduced-motion:reduce){
    .back-bar-arrow{transition:none;}
    .back-bar-btn:hover .back-bar-arrow{transform:none;}
  }
  /* On a phone the fixed ← bar at the top of the page already IS the back
     control, so an in-body one is a second control saying the same thing —
     stacked directly under it, which reads as a mistake. Hidden for every
     bar here, not only the auto-injected ones: a dialog that asks for its
     own bar (Thoughts, the semester builder, Change Major) gets the same
     treatment, because the phone already has the affordance. */
  @media (max-width:720px){
    .back-bar{display:none;}
  }

  /* ---------- Thoughts (js/59-thoughts.js) ---------- */
  /* A message wall reads like messages — avatar, name, a bubble, reaction
     pills tucked under it — not a stack of identical bordered cards where
     every student's post looked like a form entry. */
  .th-topbar{display:flex; align-items:center; justify-content:space-between; gap:10px;}
  .th-live{
    display:flex; align-items:center; gap:5px; flex:none;
    font-size:10.5px; font-weight:800; text-transform:uppercase; letter-spacing:.04em;
    color:#2ecc71; background:rgba(46,204,113,.14); border-radius:999px; padding:4px 10px 4px 8px;
  }
  .th-live i{width:6px; height:6px; border-radius:50%; background:currentcolor; display:block;}
  .th-live-off{color:var(--text-dim); background:var(--search-bg);}
  [dir="rtl"] .th-live, .plan-page.rtl-mode .th-live{padding:4px 8px 4px 10px;}

  .th-queued{
    font-size:12px;color:var(--connect);margin:0 0 12px;
  }
  .th-localnote{margin-top:14px;}
  .th-list{display:flex; flex-direction:column; gap:16px; margin:14px 0;}
  .th-pull-hint{
    height:var(--th-pull, 0px); overflow:hidden;
    display:flex; align-items:center; justify-content:center;
    font-size:11px; font-weight:700; color:var(--text-dim);
    transition:height .15s ease;
  }
  .th-pull-hint.th-pull-ready{color:var(--accent);}
  .th-pull-hint.th-pull-active{height:30px;}

  .th-item{display:flex; gap:10px; align-items:flex-start;}
  .th-av{
    flex:none; width:32px; height:32px; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:var(--search-bg); color:var(--accent);
    font-size:11.5px; font-weight:800; letter-spacing:.02em;
  }
  .th-body{flex:1 1 auto; min-width:0;}
  .th-head{display:flex;align-items:baseline;gap:10px;margin-bottom:4px;}
  .th-who{font-size:12px;font-weight:800;color:var(--text);}
  .th-when{font-size:10.5px;color:var(--text-dim);}
  .th-bubble{
    margin:0; font-size:13.5px; line-height:1.5; white-space:pre-wrap; word-break:break-word;
    background:var(--panel); border:1px solid var(--line); border-radius:4px 14px 14px 14px;
    padding:9px 12px; color:var(--text);
  }
  [dir="rtl"] .th-bubble, .plan-page.rtl-mode .th-bubble{border-radius:14px 4px 14px 14px;}
  /* Your own post in the wall — the same accent ring the rest of the app
     uses for "this one is yours/active", now on the bubble instead of a
     whole-card border. */
  .th-mine .th-bubble{border-color:var(--accent);}
  .th-del{
    margin-top:7px;cursor:pointer;font-family:inherit;
    font-size:11px;font-weight:700;color:var(--prereq);
    background:transparent;border:1px solid var(--line);border-radius:999px;padding:3px 10px;
  }
  .th-del:hover{border-color:var(--prereq);}
  .th-del:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .th-react{display:flex;gap:6px;margin-top:7px;}
  .th-react-btn{
    display:inline-flex;align-items:center;gap:5px;cursor:pointer;font-family:inherit;
    font-size:12px;color:var(--text-dim);background:var(--panel);border:1px solid var(--line);
    border-radius:999px;padding:3px 9px;
  }
  .th-react-btn span{font-variant-numeric:tabular-nums;}
  .th-react-btn:hover{border-color:var(--accent);color:var(--text);}
  .th-react-btn.active{border-color:var(--accent);color:var(--accent);background:color-mix(in srgb, var(--accent) 12%, transparent);}
  .th-react-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  /* The compose bar: one growing row (textarea + a circular send button),
     not a boxed form sitting above the wall it posts to. */
  .th-compose{margin-top:16px;}
  .th-compose-row{display:flex; align-items:flex-end; gap:8px;}
  .th-compose textarea{
    flex:1 1 auto; min-width:0; resize:none; height:40px; max-height:110px;
    background:var(--search-bg);color:var(--text);
    border:1px solid var(--line);border-radius:20px;padding:9px 14px;
    font-family:inherit;font-size:13.5px;line-height:1.4;
  }
  .th-compose textarea:focus{outline:2px solid var(--accent);outline-offset:1px;}
  .th-send{
    flex:none; width:38px; height:38px; border-radius:50%; padding:0;
    display:flex; align-items:center; justify-content:center;
    background:var(--accent); color:var(--on-accent); border:none; cursor:pointer;
  }
  .th-send .app-icon-svg{color:var(--on-accent);}
  [dir="rtl"] .th-send .app-icon-svg svg, .plan-page.rtl-mode .th-send .app-icon-svg svg{transform:scaleX(-1);}
  .th-send:hover{filter:brightness(1.08);}
  .th-send:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .th-count{display:block; font-size:10.5px; color:var(--text-dim); font-variant-numeric:tabular-nums; text-align:right; margin-top:4px;}
  [dir="rtl"] .th-count, .plan-page.rtl-mode .th-count{text-align:left;}
  .th-error{
    margin:0 0 8px;padding:9px 11px;border-radius:8px;
    background:rgba(224,83,140,.14);border:1px solid rgba(224,83,140,.45);
    color:var(--text);font-size:12.5px;line-height:1.5;
  }

  @media (max-width:720px){
    /* Pinned to the bottom of the screen. position:sticky looked right at
       first but this app's phone-modal scroll container isn't consistently
       .modal-card (some browsers/layouts scroll the document behind the
       fixed overlay instead) — the sticky threshold ended up wrong and
       later messages painted through the compose bar instead of under it.
       position:fixed sidesteps the question entirely: it is always relative
       to the viewport, the same proven pattern .asst-panel/.fix-panel and
       the toasts already use elsewhere in this file. #thList gets bottom
       padding so the last message can still scroll clear of the bar
       instead of hiding behind it. */
    #thoughtsModalOverlay .th-compose{
      position:fixed; left:0; right:0; bottom:0; z-index:1350;
      margin:0; padding:10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
      background:var(--bg); border-top:1px solid var(--line);
    }
    #thoughtsModalOverlay #thList{padding-bottom:76px;}
  }

  /* ---------- Contributions (js/73-contribute.js) ---------- */
  .contrib-list{display:grid;gap:10px;margin-top:10px;}
  .contrib-row{
    background:var(--panel);border:1px solid var(--line);border-radius:10px;
    padding:11px 13px;
  }
  .contrib-row-head{display:flex;align-items:center;justify-content:space-between;gap:10px;}
  .contrib-major{font-size:13px;font-weight:800;color:var(--text);}
  .contrib-status{
    font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;
    padding:2px 8px;border-radius:999px;background:var(--search-bg);color:var(--text-dim);
    white-space:nowrap;
  }
  .contrib-status-replied{background:rgba(46,204,113,.16);color:var(--unlock);}
  .contrib-meta{font-size:11px;color:var(--text-dim);margin-top:3px;}
  .contrib-reply{
    margin:8px 0 0;padding:8px 10px;border-radius:8px;
    background:var(--search-bg);border:1px solid var(--line);
    font-size:12.5px;line-height:1.5;color:var(--text);
  }

  /* Announced by screen readers, never drawn — the sighted equivalent of
     these messages is the plan repainting itself. */
  .sr-only{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
  }

  /* Phones: the tick was 29x29 including its padding, under the 44px every
     touch guideline asks for, and 13 cards were 43px tall. The 44px is a
     tap-target size, not a badge size — see .course-check::before above —
     so the padding grows here without the visible circle growing with it. */
  @media (max-width:720px){
    .course-check{width:18px;height:18px;padding:13px;margin:-13px;font-size:12px;}
    .course-check::before{inset:13px;}
    .course .name{padding-right:28px;}
    .plan-page.rtl-mode .course .name,
    .sheet.rtl-mode .course .name{padding-right:0;padding-left:28px;}
    .course{min-height:calc(64px * var(--card-scale, 1));}
  }

  /* ---- Edit-mode grid restyle — card-style semester columns sitting
     side by side within a year, instead of the normal stacked list. Scoped
     entirely to .editing so the regular student-facing view (and every
     other .imp-year-block/.imp-semester-block/.course consumer, like
     Degree Audit exports) is completely untouched. ---- */
  .editing .imp-year-block{display:flex;flex-wrap:wrap;align-items:flex-start;gap:14px;}
  .editing .imp-year-header{flex:1 1 100%;margin-bottom:4px;}
  .editing .imp-semester-block{
    flex:1 1 220px;min-width:200px;max-width:280px;margin-bottom:0;
    background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:10px;
    padding:12px;
  }
  .editing .imp-semester-block .course-row{
    display:flex;flex-direction:column;flex-wrap:nowrap;gap:8px;
  }
  .editing .imp-semester-block .course-row .course,
  .editing .imp-semester-block .course-row .imp-add-course-card{
    flex:0 0 auto;max-width:none;min-width:0;width:100%;
  }
  .editing .imp-semester-block .course-row .imp-add-course-card{min-height:36px;}

  /* Edit mode used to repaint every card as a pale panel with a 4px colour
     edge, which made the screen you rearrange the plan in the one screen
     that did not look like the plan. It inherits the filled requirement
     colours now — same card, same colour, whichever mode you are in. Only
     the corner radius and the drag affordance are edit-mode's own. */
  .editing .course{border-radius:10px;}

  .editing .course-cr{
    display:inline-block;margin-top:6px;padding:1px 7px;border-radius:999px;
    border:1px solid var(--line);font-weight:700;opacity:1;
  }

  /* ---- icon picker (New Study Plan dialog) ---- */
  .icon-picker-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;}
  .icon-picker-option{
    background:rgba(255,255,255,.03);border:2px solid var(--line);border-radius:8px;
    font-size:22px;padding:10px 0;cursor:pointer;text-align:center;
    transition:border-color .15s ease,background .15s ease;
  }
  .icon-picker-option:hover{border-color:var(--accent);}
  .icon-picker-option.selected{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 12%, transparent);}
  .form-field-row{display:flex;gap:8px;}
  .form-field-row .form-field{flex:1;}
  /* ================= PLAN EDITOR (developer drag-and-drop) ================= */

  .dev-edit-link{
    position:absolute; top:12px; right:14px; z-index:6;
    background:rgba(224,83,140,.15); border:1px solid var(--prereq); color:#ff9ecb;
    border-radius:999px; padding:4px 12px; font-size:11px; font-weight:800;
    cursor:pointer; font-family:inherit; letter-spacing:.03em;
  }

  .dev-edit-link:hover{background:rgba(224,83,140,.28);}


  .edit-mode-banner{
    display:none; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;
    background:linear-gradient(90deg, rgba(224,83,140,.18), rgba(224,83,140,.05));
    border:1px solid var(--prereq); border-radius:8px;
    margin:14px 32px 0; padding:10px 18px;
    font-size:12.5px; color:#ff9ecb; font-weight:700;
  }

  .plan-page.editing .edit-mode-banner{display:flex;}

  /* Public "Enter Edit Mode" toolbar button on the built-in AI majors — same
     unrestricted entry point every imported/custom plan already has (no
     Developer password). Hidden once editing (the edit-mode-banner's own
     Exit button takes over) so there's never a redundant enter button. */
  .plan-page.editing .header-actions .enter-edit-btn{display:none;}

  .edit-mode-banner .emb-exit{
    background:transparent; border:1px solid #ff9ecb; color:#ff9ecb;
    border-radius:999px; padding:5px 14px; font-size:11.5px; font-weight:800;
    cursor:pointer; font-family:inherit;
  }

  .edit-mode-banner .emb-exit:hover{background:rgba(255,158,203,.15);}

  .edit-mode-banner .emb-links{
    background:rgba(255,158,203,.14); border:1px solid #ff9ecb; color:#ff9ecb;
    border-radius:999px; padding:5px 14px; font-size:11.5px; font-weight:800;
    cursor:pointer; font-family:inherit; margin-inline-start:auto;
  }
  .edit-mode-banner .emb-links:hover{background:rgba(255,158,203,.28);}

  /* ---- structure editing on the built-in majors (edit mode only) ---- */
  .bi-year-actions,.bi-structure-actions{display:none;}
  .plan-page.editing .bi-year-actions{
    display:flex; gap:8px; flex-wrap:wrap; align-items:center;
    flex-basis:100%; margin-top:8px; padding-inline-start:2px;
  }
  .plan-page.editing .bi-structure-actions{
    display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin:16px 32px;
  }
  .bi-structure-note{font-size:11.5px;color:var(--text-dim);font-weight:600;}

  /* ---- prerequisite line editor ---- */
  .line-add-row{
    display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap;
    margin:14px 0; padding:12px; border:1px solid var(--line);
    border-radius:10px; background:var(--search-bg);
  }
  .line-add-row label{
    display:flex; flex-direction:column; gap:4px; flex:1 1 200px; min-width:0;
    font-size:11.5px; color:var(--text-dim); font-weight:700;
  }
  .line-add-row select{
    background:var(--card-bg); border:1px solid var(--line); border-radius:7px;
    padding:7px 9px; color:var(--text); font-size:12.5px; font-family:inherit; max-width:100%;
  }
  .line-add-row select:focus{outline:none;border-color:var(--accent);}
  .line-arrow{color:var(--text-dim);font-size:16px;padding-bottom:8px;}

  .line-list-head{
    display:flex; align-items:center; justify-content:space-between; gap:8px;
    flex-wrap:wrap; margin-top:16px; font-size:12px; font-weight:800; color:var(--text-dim);
  }
  .line-reset-btn{font-size:11.5px;padding:4px 10px;}
  .line-list{
    max-height:300px; overflow-y:auto; margin-top:8px; padding-inline-end:6px;
    display:flex; flex-direction:column; gap:5px;
    scrollbar-width:thin; scrollbar-color:var(--line) transparent;
  }
  .line-list::-webkit-scrollbar{width:5px;}
  .line-list::-webkit-scrollbar-track{background:transparent;}
  .line-list::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px;}
  .line-item{
    display:flex; align-items:center; gap:8px; padding:7px 10px;
    border:1px solid var(--line); border-radius:8px; background:var(--card-bg);
  }
  .line-text{flex:1 1 auto;min-width:0;font-size:12px;color:var(--text);}
  .line-text b{color:var(--prereq);padding:0 2px;}
  .line-badge{
    flex:0 0 auto; font-size:10px; font-weight:800; color:var(--unlock);
    border:1px solid var(--unlock); border-radius:999px; padding:2px 7px; white-space:nowrap;
  }
  .line-remove{
    flex:0 0 auto; background:none; border:none; color:var(--text-dim);
    font-size:15px; line-height:1; cursor:pointer; padding:2px 5px; border-radius:6px;
  }
  .line-remove:hover{color:#ff5d5d;background:rgba(255,93,93,.12);}

  /* ---- leftover data after a plan update ---- */
  .orphan-plan{
    border:1px solid var(--line); border-radius:10px; padding:10px;
    margin-bottom:10px; background:var(--search-bg);
  }
  .orphan-plan-name{font-size:11.5px;font-weight:800;color:var(--text-dim);margin-bottom:8px;}
  .orphan-item{
    display:flex; align-items:center; gap:8px; flex-wrap:wrap;
    padding:7px 0; border-top:1px solid var(--line);
  }
  .orphan-plan .orphan-item:first-of-type{border-top:none;}
  .orphan-text{flex:1 1 180px;min-width:0;display:flex;flex-direction:column;gap:2px;}
  .orphan-text b{font-size:12.5px;color:var(--text);word-break:break-word;}
  .orphan-kinds{font-size:11px;color:var(--text-dim);}
  .orphan-keep,.orphan-remove{font-size:11.5px;padding:4px 12px;flex:0 0 auto;}
  .orphan-remove{border-color:#ff5d5d;color:#ff8f8f;}
  .orphan-remove:hover{background:rgba(255,93,93,.12);}

  .acct-row{display:inline-flex;align-items:stretch;gap:4px;}
  .acct-delete-btn{padding:6px 10px;border-color:#ff5d5d;color:#ff8f8f;flex:0 0 auto;}
  .acct-delete-btn:hover{background:rgba(255,93,93,.12);}


  .editing .course[id]{
    cursor:grab;
    /* Without this, a touch-and-drag on mobile gets fought over by the
       browser's own scroll/text-selection/long-press-callout gestures —
       that fight is what was causing drags to visually "stick" until an
       unrelated extra tap reset things. This hands the whole gesture to
       our own pointer handlers instead. */
    touch-action:none;
    -webkit-user-select:none;
    user-select:none;
    -webkit-touch-callout:none;
  }

  .editing .course[id]:active{cursor:grabbing;}

  /* ---- pick up, then put down (js/85-carry.js) ----
     A tap lifts a course; every semester grows a slot; a tap on one lands
     it. The drag is untouched — this is the same move by a route that does
     not need both ends of a four-screen page visible at once. ---- */

  /* The card stays where it is while it is in hand. Removing it from the
     grid would reflow the page under the finger that just tapped it. */
  .carry-source{
    opacity:.42;
    outline:2px dashed var(--accent); outline-offset:2px;
  }

  .carry-slot{
    display:block; width:100%; margin-top:6px;
    padding:9px 10px; border-radius:9px;
    font:inherit; font-size:11.5px; font-weight:700; line-height:1.3;
    text-align:center; cursor:pointer;
    border:1px dashed var(--unlock);
    background:color-mix(in srgb, var(--unlock) 12%, transparent);
    color:var(--unlock);
    transition:background .12s ease;
  }
  .carry-slot:hover{background:color-mix(in srgb, var(--unlock) 22%, transparent);}
  .carry-slot:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}

  /* A semester the prerequisites rule out. Shown rather than hidden, with
     the reason on it: "you cannot put it there" is an answer, and a slot
     that silently is not there is not. */
  .carry-slot-no{
    border-color:var(--line); background:none; color:var(--text-dim);
    cursor:not-allowed; font-weight:400; font-style:italic;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  /* The retake case — allowed, but it is a claim about taking two things at
     once, so it does not look like the ordinary green slot. */
  .carry-slot-ask{border-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, transparent); color:var(--warn);}
  .carry-slot-here{
    border-style:solid; border-color:var(--accent); background:none;
    color:var(--accent); cursor:default; font-weight:400;
  }

  /* What you are holding, and the way to put it down again. Above the phone
     tab bar, clear of the home indicator. */
  .carry-bar{
    position:fixed; z-index:1400;
    left:12px; right:12px; bottom:calc(78px + env(safe-area-inset-bottom, 0px));
    display:flex; align-items:center; gap:10px;
    padding:10px 12px; border-radius:12px;
    background:var(--panel); border:1px solid var(--accent);
    box-shadow:0 14px 34px -16px rgba(0,0,0,.75);
    opacity:0; transform:translateY(8px);
    transition:opacity .18s ease, transform .18s ease;
  }
  .carry-bar.open{opacity:1; transform:none;}
  .carry-bar .cb-lbl{
    font-size:10px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
    color:var(--accent); flex:0 0 auto;
  }
  .carry-bar .cb-name{
    font-size:13px; font-weight:700; color:var(--text);
    flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .carry-bar .cb-x{
    flex:0 0 auto; display:inline-flex; align-items:center; gap:5px;
    padding:6px 10px; border-radius:99px; cursor:pointer;
    background:none; border:1px solid var(--line);
    font:inherit; font-size:12px; font-weight:700; color:var(--text-dim);
  }
  .carry-bar .cb-x:hover{color:var(--text); border-color:var(--text-dim);}

  /* On a wide screen there is no tab bar to clear, and the bar reads better
     as a pill in the corner than as a full-width strip. */
  @media (min-width:900px){
    .carry-bar{left:auto; right:24px; bottom:24px; width:max-content; max-width:min(460px, calc(100vw - 48px));}
  }

  /* ==========================================================================
     16. TOASTS & MISC — the small bottom-of-screen toast notification and the
     corner watermark
     ========================================================================== */
  /* ---------- AAUP logo + language toggle ---------- */

  .logo-img{
    width:34px;height:34px;object-fit:contain;border-radius:6px;
    background:#fff;padding:3px;flex:0 0 auto;
  }

  .toast-msg{
    position:fixed;bottom:22px;left:50%;transform:translateX(-50%);
    background:var(--panel);border:1px solid var(--accent);color:var(--text);
    padding:10px 18px;border-radius:999px;font-size:12.5px;font-weight:600;
    box-shadow:0 6px 24px rgba(0,0,0,.35);z-index:1200;
    opacity:0;transition:opacity .25s ease, transform .25s ease;
    pointer-events:none;white-space:nowrap;
  }

  .toast-msg.show{opacity:1;transform:translateX(-50%) translateY(-6px);}

  /* Like .toast-msg but interactive — carries an inline action button
     (e.g. "Undo" after a drag move). Separate element so it never fights
     the plain, pointer-events:none status toast. */
  .toast-action{
    position:fixed;bottom:22px;left:50%;transform:translateX(-50%);
    background:var(--panel);border:1px solid var(--accent);color:var(--text);
    padding:11px 11px 11px 15px;font-size:12.5px;font-weight:600;
    box-shadow:0 6px 24px rgba(0,0,0,.35);z-index:1201;
    display:flex;align-items:center;gap:12px;
    opacity:0;transition:opacity .25s ease, transform .25s ease;
    /* A pill is right for "Moved — saved." and wrong for a sentence: with no
       width limit and a 999px radius, the leftover-data notice wrapped into a
       tall box and the radius drew it as an ellipse with the text spilling
       out of it. A card that is allowed to wrap, and a width it cannot pass. */
    /* left:50% leaves a fixed-position box only the right half of the screen
       to shrink-to-fit into, so without max-content it laid itself out 195px
       wide on a 390px phone and grew downwards instead. */
    width:max-content; max-width:min(520px, calc(100vw - 28px));
    border-radius:16px; text-align:start;
  }
  .toast-action .ta-text{flex:1 1 auto; min-width:0; line-height:1.45;}
  .toast-action.show{opacity:1;transform:translateX(-50%) translateY(-6px);}
  .toast-action .ta-btn{
    background:var(--accent);color:var(--on-accent);border:none;border-radius:999px;
    flex:0 0 auto;align-self:center;
    padding:6px 16px;font-size:12.5px;font-weight:800;font-family:inherit;cursor:pointer;
    white-space:nowrap;
  }
  .toast-action .ta-btn:hover{filter:brightness(1.08);}

  /* The visible half of "say it out loud" — two lines, so a rounded card
     rather than the one-line pill the plain toasts use. Sits above them
     (z-index) since an unlock is the more consequential of the two. */
  .unlock-toast{
    position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(8px);
    max-width:min(420px, calc(100vw - 32px));
    display:flex;align-items:flex-start;gap:10px;
    background:var(--panel);border:1px solid var(--unlock);color:var(--text);
    padding:12px 16px;border-radius:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.4);z-index:1202;
    opacity:0;pointer-events:none;transition:opacity .25s ease, transform .25s ease;
  }
  .unlock-toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
  .unlock-toast .ut-icon{
    flex:0 0 auto;width:22px;height:22px;border-radius:50%;
    background:var(--unlock);color:var(--panel);font-size:12px;font-weight:800;
    display:flex;align-items:center;justify-content:center;margin-top:1px;
  }
  .unlock-toast .ut-body{display:flex;flex-direction:column;gap:2px;min-width:0;}
  .unlock-toast .ut-title{font-size:13px;font-weight:800;line-height:1.35;}
  .unlock-toast .ut-sub{font-size:12px;color:var(--text-dim);line-height:1.4;}
  .unlock-toast .ut-sub[hidden]{display:none;}

  /* Respect the OS-level "reduce motion" accessibility setting. */
  @media (prefers-reduced-motion: reduce){
    *, *::before, *::after{transition:none !important; animation:none !important;}
  }

  /* ==========================================================================
     17b. APP SIDEBAR — persistent left-hand navigation shown whenever a
     plan is selected (Dashboard, My Study Plan, and everything reachable
     from them). Fixed in place on desktop; becomes a slide-out drawer on
     narrow screens so it doesn't eat into the available width there.
     ========================================================================== */
  .app-sidebar{
    position:fixed; top:0; left:0; bottom:0; width:230px; z-index:900;
    background:var(--header-bg); border-right:1px solid var(--line);
    display:flex; flex-direction:column; padding:18px 0; overflow-y:auto;
    transition:transform .25s ease;
  }
  .app-sidebar .sb-brand{
    display:flex; align-items:center; gap:10px; padding:0 18px 16px; margin-bottom:8px;
    border-bottom:1px solid var(--line); font-weight:800; font-size:14px;
  }
  .app-sidebar .sb-brand .sb-mark{font-size:20px;}
  .app-sidebar .sb-section-label{
    font-size:10.5px; font-weight:800; color:var(--text-dim); text-transform:uppercase;
    letter-spacing:.05em; padding:14px 18px 6px;
  }
  .app-sidebar .sb-item{
    display:flex; align-items:center; gap:11px; padding:10px 18px; cursor:pointer;
    color:var(--text-dim); font-size:13px; font-weight:600; border:none; background:none;
    width:100%; text-align:left; font-family:inherit; border-left:3px solid transparent;
  }
  .app-sidebar .sb-item:hover{background:rgba(255,255,255,.04); color:var(--text);}
  .app-sidebar .sb-item.active{
    background:color-mix(in srgb, var(--accent) 10%, transparent); color:var(--accent); border-left-color:var(--accent); font-weight:700;
  }
  .app-sidebar .sb-icon{font-size:16px; width:20px; text-align:center; flex:0 0 auto;}
  .app-sidebar .sb-spacer{flex:1;}
  .app-sidebar .sb-switch{border-top:1px solid var(--line); margin-top:8px; padding-top:12px;}

  /* The Advanced disclosure. Styled as a row of the same weight as the items
     it hides so it reads as one more destination in the list, with a count so
     a student can see there is something behind it without opening it. */
  .app-sidebar .sb-adv-toggle{
    display:flex; align-items:center; gap:11px; width:100%;
    padding:10px 18px; background:none; border:none; border-left:3px solid transparent;
    color:var(--text-dim); font-family:inherit; font-size:13.5px; font-weight:600;
    cursor:pointer; text-align:start;
  }
  .app-sidebar .sb-adv-toggle:hover{background:rgba(255,255,255,.04); color:var(--text);}
  .app-sidebar .sb-adv-toggle.open{color:var(--text);}
  .app-sidebar .sb-adv-label{flex:1; min-width:0;}
  .app-sidebar .sb-adv-count{
    font-size:11px; font-weight:800; color:var(--text-dim);
    background:rgba(255,255,255,.06); border-radius:999px; padding:1px 7px;
    font-variant-numeric:tabular-nums;
  }
  .app-sidebar .sb-adv-chevron{color:var(--text-faint); font-size:13px; flex:none;}
  .app-sidebar .sb-adv-panel[hidden]{display:none;}

  /* .sb-groups is the phone-only grouped rendering of the same ITEMS list
     (js/37-sidebar.js moreGroupsHtml) — hidden here so desktop, which never
     asked for grouped sections, keeps exactly the flat list it always had. */
  .app-sidebar .sb-groups{display:none;}

  .sb-toggle-btn{
    display:none; position:fixed; top:14px; left:14px; z-index:950;
    width:38px; height:38px; border-radius:8px; border:1px solid var(--line);
    background:var(--header-bg); color:var(--text); font-size:16px; cursor:pointer;
  }
  .sb-overlay{display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:899;}

  /* A rendered plan page's own width and vertical rhythm. This used to be a
     style attribute on the element, which quietly won against the sidebar
     offset below — inline styles outrank every stylesheet rule — so on a
     laptop-width screen the page sat at margin-left:20px and the first ~190px
     of it disappeared behind the fixed 230px sidebar. */
  .sheet-plan{max-width:1200px;margin:20px auto;}
  .sheet-plan-simple{max-width:1100px;}

  body.has-sidebar .dashboard-page,
  body.has-sidebar .sheet{margin-left:230px;}

  @media (max-width:900px){
    .app-sidebar{transform:translateX(-100%);}
    .app-sidebar.open{transform:translateX(0);}
    /* Shown between 721 and 900px, where the sidebar collapses but the
       bottom tab bar (which owns "More") has not appeared yet. Below 720px
       both existed and opened the same drawer, so the screen carried two
       identical menu buttons — see the rule after this block. */
    .sb-toggle-btn{display:block;}
    .sb-overlay.open{display:block;}
    body.has-sidebar .dashboard-page,
    body.has-sidebar .sheet{margin-left:0;}
  }

  /* Below the tab bar's own breakpoint, "More" is one of its four tabs —
     a drawer sliding in from the left reads as a second, unrelated
     navigation model sitting next to the tab bar rather than the same
     one continuing. A sheet rising from the bottom, the same edge the
     tab bar itself lives on, reads as that tab opening rather than a
     different control appearing. Same .app-sidebar markup throughout —
     only the geometry changes here, nothing added or removed. */
  @media (max-width:720px){
    .app-sidebar{
      left:0; right:0; top:auto; bottom:0; width:auto; height:min(78vh, 600px);
      border-right:none; border-top:1px solid var(--line);
      border-radius:24px 24px 0 0; box-shadow:0 -20px 50px rgba(0,0,0,.5);
      transform:translateY(100%); padding-top:0;
    }
    .app-sidebar.open{transform:translateY(0);}
    /* A real (not absolutely-positioned) first flex item, so it scrolls
       away with the rest of the list rather than fighting overflow:auto
       to stay pinned — the handle only needs to read as "this sheet drags
       down," not remain visible mid-scroll. */
    .app-sidebar::before{
      content:''; flex:none; display:block; width:38px; height:5px; border-radius:99px;
      background:var(--line); margin:12px auto 6px;
    }
    .app-sidebar .sb-flat-list{display:none;}
    .app-sidebar .sb-groups{display:block; padding:0 14px 20px;}

    /* Grouped rows (mockup-matched): a labeled section header, then a
       single elevated card per section holding its rows — one shadow per
       group instead of a border or chevron bolted onto each flat row. */
    .app-sidebar .sb-group-label{
      font-size:10.5px; font-weight:800; color:var(--text-dim); text-transform:uppercase;
      letter-spacing:.06em; margin:16px 4px 8px;
    }
    .app-sidebar .sb-group{
      background:var(--panel); border-radius:16px; box-shadow:0 4px 14px rgba(0,0,0,.22);
      overflow:hidden;
    }
    .app-sidebar .sb-group-solo{margin-bottom:4px;}
    .app-sidebar .sb-mrow{
      display:flex; align-items:center; gap:13px; width:100%; padding:13px 15px;
      background:none; border:none; color:var(--text); font-family:inherit;
      font-size:14px; font-weight:600; cursor:pointer; text-align:start;
    }
    .app-sidebar .sb-mrow + .sb-mrow{border-top:1px solid var(--line);}
    .app-sidebar .sb-mrow-icon{
      width:34px; height:34px; flex:none; border-radius:10px;
      background:color-mix(in srgb, var(--accent) 14%, transparent); color:var(--accent);
      display:flex; align-items:center; justify-content:center;
    }
    .app-sidebar .sb-mrow-label{flex:1; min-width:0;}
    .app-sidebar .sb-mrow-chevron{color:var(--text-faint); font-size:17px; flex:none;}
    .app-sidebar .sb-mrow.active .sb-mrow-label{color:var(--accent); font-weight:800;}
    .app-sidebar .sb-mrow.active .sb-mrow-icon{background:var(--accent); color:var(--on-accent);}

    /* The install row (js/89-install.js) sits above every group and carries a
       second line the other rows do not, because it is an offer rather than a
       destination and has to say what accepting it does. */
    .app-sidebar .sb-group-install{margin-bottom:8px;}
    .app-sidebar .sb-mrow-install .sb-mrow-icon{
      background:var(--accent); color:var(--on-accent);
    }
    .app-sidebar .sb-mrow-install .sb-mrow-label{
      display:flex; flex-direction:column; gap:2px;
    }
    .app-sidebar .sb-mrow-install .sb-mrow-label b{font-weight:700;}
    .app-sidebar .sb-mrow-install .sb-mrow-label small{
      font-size:11px; font-weight:600; color:var(--text-dim);
    }

    /* On the phone sheet the toggle is a section header that can be pressed,
       so it takes the .sb-group-label's shape rather than the desktop row's —
       otherwise it would look like a destination sitting outside any card. */
    .app-sidebar .sb-adv-toggle{
      width:100%; padding:16px 4px 8px; gap:8px; border-left:none;
      font-size:10.5px; font-weight:800; text-transform:uppercase; letter-spacing:.06em;
    }
    .app-sidebar .sb-adv-toggle .sb-icon{display:none;}
    .app-sidebar .sb-adv-toggle:hover{background:none;}
    .app-sidebar .sb-adv-label{flex:0 1 auto;}
    .app-sidebar .sb-adv-chevron{margin-inline-start:auto; font-size:15px;}
  }

  /* ==========================================================================
     17. DASHBOARD — the landing screen once a plan has been chosen; a
     summary of My Progress / GPA / Achievements for that plan plus quick
     links, rather than dropping straight into the full course grid.
     ========================================================================== */
  .dashboard-page{max-width:1100px;margin:20px auto;padding:0 20px 40px;}
  /* Subtle "console" flavor: cards get a soft accent glow on hover — enough
     personality to feel alive, restrained enough to stay formal. */
  .dash-card, .dash-quicklink, .plan-card{transition:border-color .18s ease, box-shadow .18s ease, transform .15s ease;}
  .dash-card:hover, .plan-card:hover{border-color:var(--accent); box-shadow:0 0 18px color-mix(in srgb, var(--accent) 14%, transparent), 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent) inset;}
  .app-sidebar .sb-brand .sb-mark{filter:drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 50%, transparent));}
  .dash-header{
    display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px;
    background:var(--header-bg);border:1px solid var(--line);border-radius:12px;
    padding:18px 24px;margin-bottom:20px;
  }
  .dash-header .dash-title{display:flex;align-items:center;gap:12px;}
  .dash-header .dash-icon{font-size:28px;}
  .dash-header h1{margin:0;font-size:19px;}
  .dash-header p{margin:2px 0 0;font-size:12px;color:var(--text-dim);}
  .dash-actions{display:flex;gap:8px;flex-wrap:wrap;}
  .dash-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:20px;}
  .dash-swipe-dots{display:none;}
  /* Phone hero (js/36-dashboard.js's phoneHeroHtml) — one progress ring plus
     two stat chips replaces the three-tile carousel entirely below the
     breakpoint; desktop never sees this block (display:none here, no phone
     media query overrides it back on). */
  .dash-phone-hero{display:none;}
  @media (max-width:720px){
    .dash-swipe-dots, .dash-grid{display:none;}
    .dash-phone-hero{
      display:block; background:var(--panel); border-radius:20px; padding:18px 16px;
      margin-bottom:16px; box-shadow:0 8px 24px rgba(0,0,0,.28);
    }
    .dph-ring-wrap{display:flex; align-items:center; gap:16px; position:relative;}
    .dph-ring-glow{
      position:absolute; left:-14px; top:-14px; width:130px; height:130px; z-index:0;
      background:radial-gradient(circle, color-mix(in srgb, var(--accent) 28%, transparent), transparent 70%); filter:blur(4px);
    }
    .dph-ring{position:relative; width:100px; height:100px; flex:none; z-index:1;}
    .dph-ring svg{width:100%; height:100%; transform:rotate(-90deg);}
    .dph-ring .track{fill:none; stroke:var(--header-bg); stroke-width:9;}
    .dph-ring .val{fill:none; stroke:var(--accent); stroke-width:9; stroke-linecap:round; transition:stroke-dashoffset .4s ease;}
    .dph-ring-center{position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;}
    .dph-ring-center .n{font-size:21px; font-weight:800; color:var(--text); line-height:1;}
    .dph-ring-center .l{font-size:8.5px; color:var(--text-dim); font-weight:700; letter-spacing:.05em; text-transform:uppercase; margin-top:3px;}
    .dph-stat-stack{display:flex; flex-direction:column; gap:9px; z-index:1; flex:1; min-width:0;}
    .dph-stat{background:var(--header-bg); border-radius:12px; padding:9px 12px;}
    .dph-stat .n{font-size:16px; font-weight:800; color:var(--text);}
    .dph-stat .l{font-size:10px; color:var(--text-dim); font-weight:600;}

    /* "Do this next" becomes the glanceable continue card the ring sits
       above — a border+glow instead of the same flat panel every other
       card on the screen uses, since this is the one action worth acting
       on immediately. */
    .wn-lead{
      background:linear-gradient(135deg, var(--header-bg), var(--panel));
      border:1px solid color-mix(in srgb, var(--accent) 35%, transparent); border-radius:18px; padding:16px 17px;
      box-shadow:0 10px 26px rgba(0,0,0,.3);
    }
    .wn-lead-label{color:var(--connect);}
    .wn-lead-name{font-size:16.5px;}
  }
  .dash-card{
    background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px;
  }
  .dash-card h3{margin:0 0 10px;font-size:13px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.03em;}
  .dash-card .dash-big{font-size:28px;font-weight:800;color:var(--text);}
  .dash-card .dash-sub{font-size:12px;color:var(--text-dim);margin-top:4px;}

  /* 29 · The plan-health grade, above the numbers that produced it. The
     letter is the largest thing on the dashboard because it is the one thing
     the screen is now for; everything under it exists so the letter can be
     disagreed with rather than taken on faith. */
  .ph-card{margin-bottom:16px;}
  .ph-head{display:flex; align-items:center; gap:14px;}
  .ph-grade{
    font-size:38px; font-weight:800; line-height:1; color:var(--accent);
    font-variant-numeric:tabular-nums;
  }
  .ph-head-text{display:flex; flex-direction:column; gap:2px; min-width:0;}
  .ph-lbl{
    font-size:10px; font-weight:800; letter-spacing:.09em; text-transform:uppercase;
    color:var(--text-dim);
  }
  .ph-verdict{font-size:14px; font-weight:800;}
  .ph-why{margin:10px 0 0; font-size:12px; color:var(--text-dim); line-height:1.6;}
  .ph-chips{display:flex; flex-wrap:wrap; gap:6px; margin-top:11px;}
  .ph-chip{
    font-size:10.5px; font-weight:800; letter-spacing:.02em;
    border-radius:999px; padding:4px 11px; border:1px solid transparent;
  }
  .ph-good{background:color-mix(in srgb, var(--skills) 16%, transparent); color:var(--skills); border-color:var(--skills);}
  .ph-watch{background:color-mix(in srgb, var(--warn) 16%, transparent); color:var(--warn); border-color:var(--warn);}
  .ph-bad{background:color-mix(in srgb, var(--free) 18%, transparent); color:var(--free); border-color:var(--free);}
  .ph-note{margin:11px 0 0; font-size:10.5px; color:var(--text-dim); line-height:1.55;}

  /* 28 · The badges as a strip on the dashboard, instead of a count in a
     tile pointing at a screen visited once. Scrolls sideways rather than
     wrapping: a wrapped second row of badges is the wall this replaced. */
  .ach-strip{
    background:var(--panel); border:1px solid var(--line); border-radius:12px;
    padding:13px 15px; margin-bottom:20px;
  }
  .ach-strip-head{
    display:flex; align-items:baseline; justify-content:space-between; gap:10px;
    margin-bottom:9px;
  }
  .ach-strip-lbl{
    font-size:10px; font-weight:800; letter-spacing:.09em; text-transform:uppercase;
    color:var(--text-dim);
  }
  .ach-strip-count{
    font-size:12px; font-weight:800; color:var(--text-dim);
    font-variant-numeric:tabular-nums;
  }
  .ach-strip-row{
    display:flex; gap:7px; overflow-x:auto; padding-bottom:2px;
  }
  .ach-pill{
    flex:0 0 auto; display:inline-flex; align-items:center; gap:6px;
    font-size:11.5px; font-weight:700; white-space:nowrap;
    background:rgba(255,255,255,.05); border:1px solid var(--line);
    border-radius:999px; padding:6px 12px; color:var(--text);
  }
  .ach-pill svg{width:15px; height:15px; flex:0 0 auto;}
  .ach-pill-next{border-style:dashed; color:var(--text-dim);}
  .ach-pill-pct{font-weight:800; color:var(--accent); font-variant-numeric:tabular-nums;}
  .ach-pill-more{
    cursor:pointer; font-family:inherit; color:var(--accent);
    border-color:color-mix(in srgb, var(--accent) 45%, transparent);
  }
  .ach-pill-more:hover{border-color:var(--accent); background:var(--panel);}
  .ach-pill-more:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}

  /* 9 · Every empty state, everywhere (js/88-empty.js). What is missing,
     what appears once it is not, and the one button that fills it. */
  .empty-card{
    display:flex; flex-direction:column; align-items:center; text-align:center;
    gap:8px; padding:22px 20px;
    background:var(--panel); border:1px dashed var(--line); border-radius:12px;
  }
  .empty-ic{
    width:48px; height:48px; border-radius:14px; display:flex;
    align-items:center; justify-content:center;
    background:rgba(255,255,255,.05); color:var(--accent);
  }
  .empty-title{font-size:14px; font-weight:800;}
  .empty-body{
    font-size:12px; color:var(--text-dim); line-height:1.65; max-width:36ch;
  }
  .empty-btn{
    margin-top:4px; font-family:inherit; font-size:12.5px; font-weight:800;
    background:var(--accent); color:var(--on-accent); border:none;
    border-radius:999px; padding:9px 20px; cursor:pointer;
  }
  .empty-btn:focus-visible{outline:2px solid var(--accent); outline-offset:3px;}
  .empty-line{
    margin:0; font-size:12px; color:var(--text-dim); line-height:1.6;
  }
  .empty-link{
    font-family:inherit; font-size:12px; font-weight:800; color:var(--accent);
    background:none; border:none; padding:0; cursor:pointer; text-decoration:underline;
  }
  .dash-quicklinks{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;}
  .dash-quicklink{
    background:var(--panel);border:1px solid var(--line);border-radius:10px;
    padding:16px;text-align:center;cursor:pointer;transition:border-color .15s ease,transform .15s ease;
    color:var(--text);text-decoration:none;display:block;
    overflow-wrap:anywhere; min-width:0;
  }
  .dash-quicklink:hover{border-color:var(--accent);transform:translateY(-2px);}
  .dash-quicklink .dq-icon{font-size:22px;display:block;margin-bottom:6px;}
  /* Icon-grid quicklinks on phone — a circular badge + label instead of a
     bordered rectangle each, same links, same click targets. Positioned
     after the base rules above (not inside the earlier phone media block
     with the ring hero) so it wins the cascade against them regardless of
     media-query nesting — a later same-specificity rule always wins. */
  @media (max-width:720px){
    /* 3, not 4 — this app's own quicklink labels ("Degree Audit & GPA",
       "Plan My Next Semester") run longer than a 4-up grid has room for
       and wrapped three lines deep, staggering every row out of line. */
    .dash-quicklinks{
      display:grid; grid-template-columns:repeat(3,1fr); gap:16px 6px; background:none; border:none; padding:0;
    }
    .dash-quicklink{
      background:none; border:none; padding:0; display:flex; flex-direction:column; align-items:center; gap:7px;
      text-align:center; overflow-wrap:normal;
    }
    .dash-quicklink:hover{transform:none;}
    .dash-quicklink .dq-icon{
      display:flex; align-items:center; justify-content:center; margin-bottom:0;
      width:46px; height:46px; border-radius:15px; background:var(--panel); box-shadow:0 6px 16px rgba(0,0,0,.25);
    }
    .dash-quicklink:active .dq-icon{transform:scale(.94);}
    .dq-label{font-size:11px; line-height:1.3;}
  }
  .dash-next-list{display:flex;flex-direction:column;gap:6px;}
  .dash-next-item{
    display:flex;justify-content:space-between;font-size:12.5px;
    background:rgba(255,255,255,.03);border-radius:6px;padding:6px 10px;
  }

  /* ==========================================================================
     18. ONBOARDING TOUR — first-time spotlight walkthroughs (Home, Dashboard,
     Study Plan page). One <div> darkens the whole screen via a 9999px spread
     box-shadow with a transparent "hole" cut exactly over the current
     target's own bounding box, so the target reads as lit up without ever
     touching its actual styles; a small card near it carries the step's
     text, an i/N counter, and Skip/Next controls. Above the sidebar
     (900) and modals (1000) since a step can legitimately point at
     something inside either.
     ========================================================================== */
  /* pointer-events:none on the whole layer — every step's spotlighted
     element is genuinely tappable, not just the one flagged interactive:
     true, because a step's own text ("tap a course to see its details")
     is an instruction to actually do that right now, not a description of
     a feature to try later. If that tap opens a REAL modal underneath
     (course details, etc.), JS detects it and hides this layer for as
     long as that modal stays open (see pauseForRealModal/resumeFromPause)
     rather than leaving a dark overlay + stale spotlight fighting a dialog
     that just opened on top of it. The tooltip card stays interactive
     itself regardless, for its own Next/Skip buttons. */
  .tut-layer{position:fixed;inset:0;z-index:1500;display:none;pointer-events:none;}
  .tut-layer.open{display:block;}
  .tut-layer.paused{visibility:hidden;}
  .tut-spotlight{
    position:fixed;
    border-radius:10px;
    /* Lighter than a typical spotlight overlay on purpose — the "hole" this
       box-shadow trick cuts is a fixed rectangle sized to the target at
       render time, but several steps invite an interaction that reveals
       content OUTSIDE that rectangle (a modal opening, a search dropdown,
       an accordion expanding). A lighter backdrop still reads as "focus
       here" without making that revealed content unreadable. */
    box-shadow:0 0 0 9999px rgba(4,7,18,.45);
    outline:2px solid var(--accent);
    outline-offset:4px;
    pointer-events:none;
    transition:top .25s ease, left .25s ease, width .25s ease, height .25s ease, box-shadow .2s ease;
  }
  /* The interactive trace step's whole point is watching lines reach OTHER
     courses elsewhere on the page — those would just be hidden under the
     same darkness a normal spotlight uses, so this step skips the dimming
     and keeps only the guide ring around the course being held. */
  .tut-spotlight.no-dim{box-shadow:none;}
  .tut-tooltip{
    position:fixed;
    max-width:300px;
    pointer-events:auto;
    background:var(--panel);
    border:1px solid var(--accent);
    border-radius:12px;
    padding:16px 18px;
    box-shadow:0 20px 55px rgba(0,0,0,.55);
    transition:top .25s ease, left .25s ease;
  }
  .tut-tooltip h4{margin:0 0 6px;font-size:14px;font-weight:800;color:var(--text);}
  .tut-tooltip p{margin:0 0 12px;font-size:12.5px;line-height:1.55;color:var(--text-dim);}
  /* One button, at the end. The step counter and the Skip that used to share
     this row went with the guided tours (js/32-tutorial.js) — a coach mark
     has no step to count and nothing to skip past. */
  .tut-tooltip .tut-foot{display:flex;align-items:center;justify-content:flex-end;}
  .tut-tooltip .tut-actions{display:flex;align-items:center;gap:14px;}
  .tut-next{
    background:var(--accent);border:none;color:var(--on-accent);
    font-size:12.5px;font-weight:800;font-family:inherit;
    padding:8px 16px;border-radius:999px;cursor:pointer;white-space:nowrap;
  }
  .tut-next:hover{filter:brightness(1.08);}
  @media (prefers-reduced-motion: reduce){
    .tut-spotlight, .tut-tooltip{transition:none;}
  }


  /* 11 · THE WATERMARK IS FOR THINGS THAT LEAVE THE APP.
     It sat fixed over every screen at z-index 9999 — on top of table rows, on
     top of the plan, on top of a student's own grades, permanently. The name
     belongs on the artefacts other people see: the printed plan and the share
     screen, both of which now carry it as real content rather than as an
     overlay. On screen it is gone; in print it comes back once, on the page. */
  .watermark-layer{display:none;}
  @media print{
    .watermark-layer{
      display:block;
      position:fixed;
      right:14px;
      bottom:10px;
      z-index:9999;
      pointer-events:none;
      color:rgba(0,0,0,.45);
      font-size:9px;
      font-weight:600;
      letter-spacing:.02em;
      font-family:-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
      white-space:nowrap;
    }
  }
  /* 45 · THE LANGUAGE SWITCH, ON THE PLAN.
     Two characters in the plan header. It used to be More → Settings →
     Preferences → Language, four taps each way, which is enough friction that
     a bilingual student simply stops switching. */
  .plan-lang{
    display:inline-flex; gap:0; border:1px solid var(--line);
    border-radius:999px; overflow:hidden; margin-top:6px;
  }
  .plan-lang-btn{
    appearance:none; border:0; cursor:pointer; font:inherit;
    font-size:11.5px; font-weight:700; letter-spacing:.02em;
    padding:5px 13px; min-width:40px;
    background:transparent; color:var(--text-dim); transition:.14s;
  }
  .plan-lang-btn:hover{color:var(--text);}
  .plan-lang-btn.is-on{background:var(--accent); color:var(--on-accent);}
  .plan-lang-btn:focus-visible{outline:2px solid var(--accent); outline-offset:-2px;}

  /* 10 · SAY THAT IT WORKS OFFLINE.
     Offline-first is the app's best feature and it was completely invisible.
     A student on campus wifi that drops needs to know BEFORE it drops. One
     line on the home screen, and it changes rather than disappearing when the
     connection actually goes — which is the moment it is worth reading. */
  .offline-note{
    display:inline-flex; align-items:center; gap:7px;
    font-size:11.5px; font-weight:600; color:var(--text-dim);
    padding:5px 11px; border-radius:999px;
    border:1px solid var(--line); background:var(--surface);
  }
  .offline-note::before{
    content:''; width:7px; height:7px; border-radius:50%;
    background:var(--ok, #3fbf7f); flex:none;
  }
  .offline-note.is-offline{color:var(--text); border-color:var(--accent);}
  .offline-note.is-offline::before{background:var(--accent);}

  /* 64 · "WHERE ARE YOU NOW?" — one tap instead of forty.
     Only on a plan with nothing marked, which is the one moment it helps. */
  .way-card{
    border:1px solid var(--accent); border-radius:14px;
    padding:14px 15px; margin:0 0 16px;
    background:var(--card-bg);
  }
  .way-head{font-size:14.5px; font-weight:700; margin-bottom:4px;}
  .way-note{font-size:12px; color:var(--text-dim); margin:0 0 11px; line-height:1.55;}
  .way-chips{display:flex; flex-wrap:wrap; gap:6px;}
  .way-chip{
    appearance:none; cursor:pointer; font:inherit;
    font-size:11.5px; font-weight:700; padding:7px 12px; border-radius:999px;
    border:1px solid var(--line); background:transparent; color:var(--text);
    transition:.12s;
  }
  .way-chip:hover{border-color:var(--accent); background:var(--search-bg);}
  .way-chip:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}

  /* 5 · GRADE CHIPS INSTEAD OF A NATIVE SELECT.
     On Android a <select> throws a full-screen list over the app to pick one
     of twelve values — twelve round trips through a modal that covers the very
     table you are filling in. */
  .gs-grade-chips{display:flex; flex-wrap:wrap; gap:4px;}
  .gs-grade-chip{
    appearance:none; cursor:pointer; font:inherit;
    font-size:11px; font-weight:700; line-height:1;
    min-width:30px; padding:6px 7px; border-radius:7px;
    border:1px solid var(--line); background:var(--card-bg); color:var(--text-dim);
    transition:.12s;
  }
  .gs-grade-chip:hover{color:var(--text); border-color:var(--text-dim);}
  .gs-grade-chip.is-on{background:var(--accent); border-color:var(--accent); color:var(--on-accent);}
  .gs-grade-chip.is-fail.is-on{background:var(--danger,#e8557c); border-color:var(--danger,#e8557c);}
  .gs-grade-chip.gs-grade-none{opacity:.65;}
  .gs-grade-chip:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
  /* What FA and W mean, once, under the grid — instead of inside every FA
     and W chip on every row, which is what made them unreadable. */
  .gs-foot{
    margin:9px 2px 0; font-size:10.5px; line-height:1.55; color:var(--text-dim);
  }

  /* 12 · MARK A WHOLE SEMESTER AT ONCE. */
  .imp-sem-bulk{
    appearance:none; cursor:pointer; font:inherit;
    font-size:10.5px; font-weight:700; letter-spacing:.02em;
    padding:3px 9px; border-radius:6px; margin-inline-start:auto;
    border:1px solid var(--line); background:transparent; color:var(--text-dim);
    transition:.12s;
  }
  .imp-sem-bulk:hover{color:var(--text); border-color:var(--accent);}
  .imp-sem-bulk:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}

  /* 50 · THE INSTALL OFFER (js/89-install.js).
     Three placements, one state. This one is the bar under the hero on Home —
     the one a stranger sees. Filled rather than outlined: it is the only
     thing on that screen asking for something rather than reporting
     something, and it disappears entirely once the app is installed. */
  /* An accent-EDGED panel, not an accent-filled one. Filling it meant
     reversing the button out in --on-accent, and --on-accent is near-black in
     the dark themes and white in the light ones — so the "white pill" became
     a black pill with blue text on half the themes. The accent/--on-accent
     pairing is already tuned per theme for exactly one thing, a filled
     button, so that is what the button is and the bar carries the edge. */
  .install-bar{
    display:flex; align-items:center; gap:12px;
    padding:12px 13px; margin:14px 0 0;
    border:1px solid var(--accent); border-radius:14px;
    background:color-mix(in srgb, var(--accent) 8%, var(--panel));
  }
  .install-ic{
    width:34px; height:34px; flex:none; border-radius:11px;
    background:color-mix(in srgb, var(--accent) 16%, transparent);
    color:var(--accent);
    display:flex; align-items:center; justify-content:center;
  }
  .install-copy{display:flex; flex-direction:column; gap:3px; flex:1; min-width:0;}
  .install-copy b{font-size:13px; font-weight:700; line-height:1.35; color:var(--text);}
  .install-copy span{font-size:11px; line-height:1.45; color:var(--text-dim);}
  .install-go{
    flex:none; appearance:none; cursor:pointer; font-family:inherit;
    font-size:12px; font-weight:800; padding:8px 16px; border-radius:999px;
    border:none; background:var(--accent); color:var(--on-accent);
    align-self:center;
  }
  .install-go:hover{filter:brightness(1.08);}
  .install-go:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}

  /* iOS Safari has no install API at all, so the button opens these two
     steps instead of doing nothing. */
  .ios-steps{
    margin:4px 0 0; padding-inline-start:22px;
    font-size:13.5px; line-height:1.75; color:var(--text);
  }
  .ios-steps li{margin-bottom:6px;}

  /* Settings → Help. The permanent one: it is here in every state, including
     "already installed", because someone who deleted the app needs a way
     back and the browser will not re-offer on its own. */
  .install-help{
    border:1px solid var(--line); border-radius:12px;
    padding:13px 14px; margin-bottom:16px; background:var(--panel);
  }
  .install-help-head{
    display:flex; align-items:center; justify-content:space-between;
    gap:12px; flex-wrap:wrap;
  }
  .install-help-t{font-size:13.5px; font-weight:700;}
  .install-help-btn{flex:none; font-size:12px;}
  .install-help-done{
    display:inline-flex; align-items:center; gap:5px; flex:none;
    font-size:11.5px; font-weight:800; color:var(--skills);
  }
  .settings-rule{
    border:0; border-top:1px solid var(--line); margin:0 0 16px;
  }

  /* The credit on the share screen, under the QR: content, not chrome. */
  .share-credit{
    margin:14px 0 0; text-align:center;
    font-size:10.5px; font-weight:600; letter-spacing:.02em;
    color:var(--text-dim); opacity:.7;
  }
  .update-bar{
    position:fixed; top:0; left:0; right:0; z-index:2000;
    background:var(--accent); color:var(--on-accent);
    padding:9px 14px; font-size:12.5px; font-weight:700;
    display:none; align-items:center; justify-content:center; gap:12px;
    text-align:center;
  }
  .update-bar button{
    background:var(--navy-900); color:var(--accent); border:none;
    padding:6px 14px; border-radius:999px; font-weight:800; font-size:12px;
    cursor:pointer; font-family:inherit; white-space:nowrap;
  }

/* Catalogue loading / unreachable state on the home grid. Grid-column span so
   it reads as one message rather than pretending to be a plan card. */
.catalogue-status{
  grid-column:1/-1; text-align:center; padding:26px 18px; line-height:1.6;
  color:var(--text-dim); font-size:13.5px;
}
.catalogue-status .home-btn{margin-top:12px;}

/* A published programme whose course list has not been transcribed yet. Muted
   rather than hidden: its existence is real information for a student choosing
   a major, but the card must not promise a page it cannot show. */
.plan-card-pending{opacity:.62;}
.plan-card-pending:hover{opacity:.78;}
.plan-card-pending .pc-cta{color:var(--text-dim); border-color:transparent;}


/* ==========================================================================
   19. ASSISTANT — launcher, chat window, and Guided Mode
   ========================================================================== */

/* The assistant's floating launcher used to live here, bottom-right. It is
   removed: the assistant is a tab in the phone tab bar and a row in the
   menu, and a third entrance floating over the plan is one entrance too
   many. Fix keeps the bottom-left corner to itself. */

.asst-panel{
  position:fixed; right:16px; bottom:100px; z-index:1401;
  width:min(370px, calc(100vw - 32px));
  max-height:min(560px, calc(100vh - 150px));
  background:var(--panel); border:1px solid var(--line); border-radius:16px;
  box-shadow:0 24px 60px rgba(0,0,0,.55);
  display:none; flex-direction:column; overflow:hidden;
}
.asst-panel.open{display:flex;}

.asst-head{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:12px 14px; border-bottom:1px solid var(--line);
  background:var(--header-bg);
}
.asst-head-title{font-size:13px; font-weight:800; color:var(--text);}
.asst-close{
  background:none; border:none; color:var(--text-dim);
  font-size:22px; line-height:1; cursor:pointer; padding:0 4px; font-family:inherit;
}
.asst-close:hover{color:var(--text);}

.asst-log{
  flex:1; overflow-y:auto; padding:14px; display:flex; flex-direction:column; gap:10px;
  -webkit-overflow-scrolling:touch;
}
.asst-msg{display:flex;}
.asst-msg.asst-user{justify-content:flex-end;}
.asst-bubble{
  max-width:88%; padding:10px 13px; border-radius:14px;
  font-size:12.8px; line-height:1.6; color:var(--text);
  background:var(--surface-bg); border:1px solid var(--line);
}
.asst-user .asst-bubble{
  background:var(--accent); color:var(--on-accent);
  border-color:transparent; font-weight:600;
}
.asst-bubble p{margin:0 0 6px;}
.asst-bubble p:last-child{margin-bottom:0;}
.asst-title{font-weight:800; margin-bottom:6px; color:var(--accent);}
.asst-warn{
  margin-top:8px; padding:8px 10px; border-radius:9px;
  background:rgba(224,83,140,.12); border:1px solid var(--prereq);
  color:var(--text); font-size:12px; line-height:1.55;
}
.asst-actions{display:flex; flex-wrap:wrap; gap:8px; margin-top:10px;}
.asst-act{
  background:none; border:1px solid var(--line); color:var(--text-dim);
  border-radius:999px; padding:6px 13px; font-size:12px; font-weight:700;
  font-family:inherit; cursor:pointer;
}
.asst-act:hover:not(:disabled){color:var(--text); border-color:var(--accent);}
.asst-act:disabled{opacity:.5; cursor:default;}
.asst-act-primary{background:var(--accent); color:var(--on-accent); border-color:transparent;}

.asst-chips{display:flex; flex-wrap:wrap; gap:7px;}
.asst-chip{
  background:var(--surface-bg); border:1px solid var(--line); color:var(--text-dim);
  border-radius:999px; padding:6px 12px; font-size:11.5px;
  font-family:inherit; cursor:pointer;
}
.asst-chip:hover{color:var(--text); border-color:var(--accent);}

.asst-form{
  display:flex; gap:8px; padding:10px 12px; border-top:1px solid var(--line);
  background:var(--header-bg);
}
.asst-input{
  flex:1; background:var(--search-bg, var(--surface-bg));
  border:1px solid var(--line); border-radius:999px;
  padding:9px 14px; color:var(--text); font-size:12.5px; font-family:inherit;
}
.asst-input:focus{outline:none; border-color:var(--accent);}
.asst-send{
  background:var(--accent); color:var(--on-accent); border:none;
  width:38px; height:38px; border-radius:50%; cursor:pointer;
  font-size:14px; font-family:inherit; flex:0 0 auto;
}

/* ---- Guided Mode --------------------------------------------------------
   Same box-shadow "cut a hole in the dark" technique as the onboarding tour,
   with pointer-events off the layer so the highlighted control is genuinely
   tappable — a step that says "tap this" must let you tap it. The pulse ring
   is a separate ::after so the spotlight itself can keep animating position
   smoothly without the two transitions fighting. */
.guide-layer{position:fixed; inset:0; z-index:1500; display:none; pointer-events:none;}
.guide-layer.open{display:block;}
.guide-spot{
  position:fixed; border-radius:12px;
  box-shadow:0 0 0 9999px rgba(4,7,18,.62);
  outline:2px solid var(--accent); outline-offset:4px;
  pointer-events:none;
  transition:top .28s ease, left .28s ease, width .28s ease, height .28s ease;
}
.guide-spot::after{
  content:''; position:absolute; inset:-6px; border-radius:16px;
  border:2px solid var(--accent); opacity:0;
  animation:guidePulse 1.8s ease-out infinite;
}
@keyframes guidePulse{
  0%{transform:scale(.96); opacity:.85;}
  70%{transform:scale(1.09); opacity:0;}
  100%{transform:scale(1.09); opacity:0;}
}
.guide-tip{
  position:fixed; pointer-events:auto;
  background:var(--panel); border:1px solid var(--accent); border-radius:12px;
  padding:15px 17px; box-shadow:0 20px 55px rgba(0,0,0,.6);
  transition:top .28s ease, left .28s ease;
}
.guide-tip h4{margin:0 0 6px; font-size:13px; font-weight:800; color:var(--accent);}
.guide-tip p{margin:0 0 12px; font-size:12.5px; line-height:1.6; color:var(--text);}
.guide-foot{display:flex; align-items:center; justify-content:space-between; gap:10px;}
.guide-step{font-size:11px; color:var(--text-dim); font-weight:700; white-space:nowrap;}
.guide-actions{display:flex; align-items:center; gap:12px;}
.guide-end{
  background:none; border:none; color:var(--text-dim);
  font-size:12px; font-family:inherit; cursor:pointer; padding:0;
}
.guide-end:hover{color:var(--text);}
.guide-next{
  background:var(--accent); border:none; color:var(--on-accent);
  font-size:12.5px; font-weight:800; font-family:inherit;
  padding:8px 16px; border-radius:999px; cursor:pointer; white-space:nowrap;
}
.guide-next:hover{filter:brightness(1.08);}

@media (prefers-reduced-motion: reduce){
  .guide-spot, .guide-tip{transition:none;}
  .guide-spot::after{animation:none;}
}


/* ==========================================================================
   20. FIX PANEL — problem report and repairs
   ========================================================================== */

/* Bottom-LEFT, on every screen, always reachable — including while a plan
   page is scrolled or a modal is open. */
.fix-launcher{
  position:fixed; left:16px; bottom:18px; z-index:1400;
  width:46px; height:46px; border-radius:50%;
  background:var(--panel); color:var(--text);
  border:1px solid var(--line); font-size:19px; line-height:1; cursor:pointer;
  box-shadow:0 8px 22px rgba(0,0,0,.4);
  display:flex; align-items:center; justify-content:center;
  transition:transform .15s ease, border-color .15s ease;
}
.fix-launcher:hover{transform:translateY(-2px); border-color:var(--accent);}

.fix-panel{
  position:fixed; left:16px; bottom:76px; z-index:1401;
  width:min(430px, calc(100vw - 32px));
  max-height:min(620px, calc(100vh - 120px));
  background:var(--panel); border:1px solid var(--line); border-radius:16px;
  box-shadow:0 24px 60px rgba(0,0,0,.55);
  display:none; flex-direction:column; overflow:hidden;
}
.fix-panel.open{display:flex;}

.fix-head{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:12px 14px; border-bottom:1px solid var(--line); background:var(--header-bg);
}
.fix-head-title{font-size:13px; font-weight:800; color:var(--text);}
.fix-close{
  background:none; border:none; color:var(--text-dim);
  font-size:22px; line-height:1; cursor:pointer; padding:0 4px; font-family:inherit;
}
.fix-close:hover{color:var(--text);}

.fix-body{flex:1; overflow-y:auto; padding:14px; -webkit-overflow-scrolling:touch;}
.fix-sub{margin:0 0 12px; font-size:12px; line-height:1.6; color:var(--text-dim);}
.fix-bar{display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-bottom:14px;}
.fix-progress{font-size:11.5px; color:var(--text-dim); font-weight:700;}

.fix-btn{
  border-radius:999px; padding:7px 14px; font-size:12px; font-weight:700;
  font-family:inherit; cursor:pointer; border:1px solid var(--line);
  background:none; color:var(--text-dim);
}
.fix-btn:hover:not(:disabled){color:var(--text); border-color:var(--accent);}
.fix-btn:disabled{opacity:.55; cursor:default;}
.fix-btn-primary{background:var(--accent); color:var(--on-accent); border-color:transparent;}
.fix-btn-ghost{font-weight:600;}

.fix-summary{display:flex; flex-wrap:wrap; gap:7px; margin-bottom:12px;}
.fix-chip{
  border-radius:999px; padding:3px 10px; font-size:10.5px; font-weight:800;
  letter-spacing:.03em; white-space:nowrap;
}
.fix-chip-critical{background:var(--free); color:var(--free-tx);}
.fix-chip-warning{background:var(--uni); color:var(--uni-tx);}
.fix-chip-info{background:var(--misc); color:var(--misc-tx);}
.fix-chip-done{background:var(--skills); color:var(--skills-tx);}

.fix-clean{
  margin:0; padding:20px 14px; text-align:center;
  font-size:13px; line-height:1.6; color:var(--text);
}
.fix-empty{margin:8px 0 0; font-size:11.5px; line-height:1.6; color:var(--text-dim);}

.fix-card{
  border:1px solid var(--line); border-radius:12px; margin-bottom:12px;
  background:var(--surface-bg); overflow:hidden;
}
.fix-card.fix-done{opacity:.85;}
.fix-sev-critical{border-left:3px solid var(--free);}
.fix-sev-warning{border-left:3px solid var(--uni);}
.fix-sev-info{border-left:3px solid var(--misc);}
[dir="rtl"] .fix-sev-critical,
[dir="rtl"] .fix-sev-warning,
[dir="rtl"] .fix-sev-info{border-left:none; border-right:3px solid var(--line);}
[dir="rtl"] .fix-sev-critical{border-right-color:var(--free);}
[dir="rtl"] .fix-sev-warning{border-right-color:var(--uni);}
[dir="rtl"] .fix-sev-info{border-right-color:var(--misc);}

.fix-card-head{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  padding:11px 13px; border-bottom:1px solid var(--line);
}
.fix-card-title{font-size:12.5px; font-weight:800; color:var(--text); line-height:1.45;}
.fix-card-body{padding:11px 13px;}

.fix-field{margin-bottom:8px; font-size:12px; line-height:1.6;}
.fix-field-label{
  display:block; font-size:10.5px; font-weight:800; letter-spacing:.04em;
  text-transform:uppercase; color:var(--text-dim); margin-bottom:2px;
}
.fix-field-value{color:var(--text); word-break:break-word;}

.fix-code{
  background:var(--page-bg); border:1px solid var(--line); border-radius:8px;
  padding:10px 12px; font-size:11px; line-height:1.55; color:var(--text);
  overflow-x:auto; white-space:pre-wrap; word-break:break-word; margin:8px 0;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.fix-card-actions{display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:10px;}
.fix-manual{font-size:11.5px; color:var(--text-dim); line-height:1.5;}
.fix-result{display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:10px;}
.fix-result-note{font-size:11.5px; color:var(--text-dim); line-height:1.5; width:100%;}

.fix-diff{
  margin-top:10px; padding:9px 11px; border-radius:8px;
  background:var(--page-bg); border:1px solid var(--line);
}
.fix-diff-row{display:flex; align-items:center; gap:8px; font-size:11px; margin-bottom:4px;}
.fix-diff-row code{color:var(--text); word-break:break-all;}
.fix-diff-kind{
  border-radius:999px; padding:1px 8px; font-size:10px; font-weight:800; white-space:nowrap;
}
.fix-diff-added{background:var(--skills); color:var(--skills-tx);}
.fix-diff-removed{background:var(--free); color:var(--free-tx);}
.fix-diff-changed{background:var(--uni); color:var(--uni-tx);}

.fix-history{
  border:1px solid var(--line); border-radius:12px; padding:11px 13px; margin-bottom:14px;
  background:var(--surface-bg);
}
.fix-history h3{margin:0 0 8px; font-size:12px; font-weight:800; color:var(--text);}
.fix-hist-row{border-top:1px solid var(--line); padding:9px 0;}
.fix-hist-row:first-of-type{border-top:none;}
.fix-hist-when{font-size:10.5px; color:var(--text-dim);}
.fix-hist-title{font-size:12px; font-weight:700; color:var(--text); margin:2px 0 3px; line-height:1.45;}
.fix-hist-note{font-size:11px; color:var(--text-dim); line-height:1.5; margin-bottom:5px;}

/* Clear of the sidebar when there is one. The bottom-left corner belongs to
   the sidebar's own "Switch Plan" item while a plan is open, and a floating
   button parked on top of it does not just look wrong — it swallows the tap.
   Reuses the same body.has-sidebar class the page content already shifts by,
   so the two can never disagree about whether a sidebar is there. */
body.has-sidebar .fix-launcher{left:calc(230px + 16px);}
body.has-sidebar .fix-panel{left:calc(230px + 16px);}

/* On a phone both panels become bottom sheets — a 370px-wide floating card
   anchored to a corner is unusable at 360px viewport width. */
@media (max-width:560px){
  /* The assistant takes the screen. It was a 370px card floating over the
     plan with roughly a third of the viewport for a conversation — a chat
     needs room to be read back, and every other dialog in this app is
     already a full page on a phone. Fix stays a floating panel: it is a
     status readout you glance at, not something you hold a conversation in.
     100dvh, not vh, for the same reason .modal-card uses it — vh counts the
     space behind the browser's own toolbar, which is where the composer
     would otherwise end up. */
  .asst-panel{
    inset:0; left:0; right:0; top:0; bottom:0;
    width:auto; max-width:none; height:100dvh; max-height:100dvh;
    border:none; border-radius:0;
  }
  .asst-panel .asst-head{
    padding-top:calc(12px + env(safe-area-inset-top, 0px));
  }
  .asst-panel .asst-log{flex:1; min-height:0;}
  .asst-panel .asst-form{
    padding-bottom:calc(10px + env(safe-area-inset-bottom, 0px));
  }
  /* Nothing floats over a full-screen conversation. */
  body:has(.asst-panel.open) .sb-tabbar,
  body:has(.asst-panel.open) .fix-launcher{display:none;}

  .fix-panel{
    left:8px; right:8px; width:auto;
    bottom:74px; max-height:calc(100vh - 130px);
  }
  .fix-launcher{left:12px; bottom:16px; width:42px; height:42px; font-size:17px;}
  /* The sidebar slides off-canvas at this width, so there is nothing to
     clear — put the button back in the corner. */
  body.has-sidebar .fix-launcher{left:12px;}
  body.has-sidebar .fix-panel{left:8px;}
}


/* ---- Smart assistant: mode toggle, thinking indicator, consent ----------
   The ✨/💤 toggle is hidden entirely until a Worker is configured, so an
   install with no AI endpoint looks exactly as it did before. */
.asst-head-actions{display:flex; align-items:center; gap:4px;}
.asst-mode{
  background:none; border:1px solid var(--line); color:var(--text-dim);
  border-radius:999px; padding:3px 9px; font-size:12px; line-height:1.4;
  cursor:pointer; font-family:inherit;
}
.asst-mode:hover{border-color:var(--accent); color:var(--text);}
.asst-mode-on{border-color:var(--accent); background:color-mix(in srgb, var(--accent) 14%, transparent);}

/* Three dots while the online brain is thinking. The offline one answers
   instantly and never shows this, which is itself a useful signal. */
.asst-typing{display:flex; gap:5px; align-items:center; padding:14px 15px;}
.asst-typing span{
  width:6px; height:6px; border-radius:50%; background:var(--text-dim);
  animation:asstBlink 1.3s infinite ease-in-out;
}
.asst-typing span:nth-child(2){animation-delay:.18s;}
.asst-typing span:nth-child(3){animation-delay:.36s;}
@keyframes asstBlink{
  0%, 80%, 100%{opacity:.25; transform:translateY(0);}
  40%{opacity:1; transform:translateY(-3px);}
}
@media (prefers-reduced-motion: reduce){
  .asst-typing span{animation:none; opacity:.6;}
}

/* The one-time opt-in. Deliberately the most prominent thing in the window
   when it appears — it is the only moment anything is ever sent off-device. */
.asst-consent{border-color:var(--accent); background:var(--panel);}
.asst-consent p{font-size:12.3px; color:var(--text-dim);}
.asst-consent p:first-of-type{color:var(--text);}

/* Bullets in an assistant reply. The online brain writes "- " lists whether
   or not you ask it to, so they are rendered rather than shown as raw text. */
.asst-list{margin:4px 0 8px; padding-inline-start:18px;}
.asst-list li{margin-bottom:3px; line-height:1.55;}
.asst-bubble strong{font-weight:800; color:var(--text);}

/* ==========================================================================
   ICONS — the three-layer mark used for a university, a college, or a major
   (js/47-icons.js). One box, whichever layer fills it: an uploaded PNG, a
   built-in line icon, or the emoji the app has always used.
   ========================================================================== */
.app-icon{display:inline-flex; align-items:center; justify-content:center; flex:0 0 auto;}

/* Panel/modal headings that used to be a plain "🏆 Achievements"-style emoji
   prefix — icon + label side by side with the same gap the rest of the app's
   icon+label rows (.home-btn, .dq-icon, .sb-icon...) already use. */
h2.mh, h3.mh{display:flex; align-items:center; gap:8px;}

/* The SVGs are drawn with currentColor, so they take the accent from whatever
   tile they sit in and follow the theme without a second set for dark mode. */
.app-icon-svg{color:var(--accent);}
/* ...except inside a control that has filled itself with the accent and set
   its own ink. The default above is a colour, not inheritance, so it won an
   accent-on-accent fight and the icon simply disappeared — which is what had
   happened to the pin on "Enter marks from my plan". */
.gs-goplan .app-icon-svg,
.au-mode-on .app-icon-svg,
.po-mode.is-on .app-icon-svg,
.status-btn.active .app-icon-svg,
.pm-btn.active .app-icon-svg,
.ach-cat-pill.active .app-icon-svg,
.ct-chip-on .app-icon-svg,
.settings-tab.active .app-icon-svg,
.share-native-btn .app-icon-svg,
.wiz-continue .app-icon-svg,
.story-continue .app-icon-svg,
.tut-next .app-icon-svg,
.update-bar .app-icon-svg,
.asst-user .asst-bubble .app-icon-svg,
.asst-act-primary .app-icon-svg,
.asst-send .app-icon-svg,
.guide-next .app-icon-svg,
.fix-btn-primary .app-icon-svg,
.cd-action-primary .app-icon-svg,
.cloud-live-avatar .app-icon-svg,
.pf-chip.pf-on .app-icon-svg,
.pg-why-locked-btn:hover .app-icon-svg,
.achievement-badge .ab-share:hover .app-icon-svg{color:inherit;}
.app-icon-svg svg{width:100%; height:100%; display:block;}

/* Beats the older `.plan-card .pc-icon svg{width:30px}` rule, which sized the
   built-in cards' inline SVGs directly. Here the wrapper owns the size and the
   SVG just fills it, so one markup() call works at every call site. */
.pc-icon .app-icon-svg svg,
.hr-icon .app-icon-svg svg,
.mark .app-icon-svg svg{width:100%; height:100%;}

/* The sidebar and its phone tab bar are the one place these icons sit next
   to a label whose colour already carries the state that matters (dim vs.
   accent for the active item/tab) — pinning the icon to the accent always
   would have every item look "selected" and erase that signal. Here the icon
   just follows whatever colour its own button is already using. */
.sb-item .app-icon-svg,
.sb-tab .app-icon-svg,
.sb-mrow-icon .app-icon-svg{color:inherit;}

/* An uploaded logo is never cropped or stretched: it is fitted, so a wide
   university wordmark and a square crest both sit correctly in the same box. */
.app-icon-img{display:block; border-radius:6px;}

/* Emoji needs no colour treatment — it carries its own. */
.app-icon-emoji{color:inherit;}

/* ==========================================================================
   ADMIN MODE (js/48-admin.js) — reached at #admin, invisible otherwise.
   Deliberately borrows the app's tokens rather than introducing an admin
   theme: the same colours, the same .home-btn, the same field styling, so it
   reads as part of the product and not a bolted-on console.
   ========================================================================== */
.admin-overlay{
  position:fixed; inset:0; z-index:9600; display:none;
  /* --page-bg, not --bg: there is no --bg token, so the overlay painted
     transparent and the whole app showed through behind the dashboard. */
  background:var(--page-bg); overflow:hidden;
}
.admin-overlay.open{display:block;}

/* A real app shell rather than a long scrolling page with sticky pieces: the
   header is a flex row that never scrolls, and the nav and the main panel each
   scroll on their own. The earlier version pinned the nav with `top:57px`,
   which is only correct while the header happens to be 57px tall — it wraps on
   narrow screens and on long usernames, and then it sat on top of the nav. */
.admin-shell{height:100%; display:flex; flex-direction:column;}

.admin-top{
  flex:0 0 auto; z-index:2; display:flex; align-items:center;
  justify-content:space-between; gap:12px; flex-wrap:wrap;
  padding:12px 16px; background:var(--panel); border-bottom:1px solid var(--line);
}
.admin-brand{font-size:15px; color:var(--text);}
.admin-brand span{color:var(--text-dim); font-weight:600;}
.admin-top-actions{display:flex; align-items:center; gap:8px;}
.admin-savestate{font-size:11.5px; color:var(--text-dim); font-weight:700;}

.admin-body{display:flex; flex:1 1 auto; min-height:0; align-items:stretch;}
.admin-nav{
  flex:0 0 208px; padding:14px 10px; border-inline-end:1px solid var(--line);
  display:flex; flex-direction:column; gap:4px; overflow-y:auto;
}
.admin-navbtn{
  text-align:start; padding:9px 11px; border-radius:9px; border:1px solid transparent;
  background:none; color:var(--text-dim); font-size:12.5px; font-weight:700; cursor:pointer;
}
.admin-navbtn:hover{background:color-mix(in srgb, var(--accent) 8%, transparent); color:var(--text);}
.admin-navbtn.is-active{background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--text); border-color:var(--accent);}
.admin-navbtn.is-off{opacity:.45;}
.admin-nav-context{
  margin-top:10px; padding:9px 11px; border-radius:9px; background:color-mix(in srgb, var(--accent) 7%, transparent);
  font-size:11px; color:var(--text-dim); line-height:1.6;
}
.admin-dot-dirty{color:var(--prereq); font-weight:800;}

.admin-main{flex:1 1 auto; min-width:0; padding:18px 20px 60px; max-width:1100px; overflow-y:auto;}
.admin-main h2{margin:0 0 6px; font-size:19px;}
.admin-main h3{margin:22px 0 8px; font-size:14px;}
.admin-main h4{margin:16px 0 6px; font-size:12.5px; color:var(--text-dim);}
.admin-sub{font-size:11.5px; color:var(--text-dim); font-weight:600;}
.admin-hint{font-size:11.5px; color:var(--text-dim); line-height:1.65; margin:4px 0 12px;}

.admin-login{max-width:380px; margin:8vh auto 0;}
.admin-primary{border-color:var(--accent); color:var(--text);}

.admin-stats{display:flex; gap:10px; flex-wrap:wrap; margin:12px 0 18px;}
.admin-stat{
  flex:1 1 130px; padding:12px 14px; border-radius:11px;
  background:var(--panel); border:1px solid var(--line);
  font-size:11.5px; color:var(--text-dim);
}
.admin-stat-n{font-size:22px; font-weight:800; color:var(--text);}

.admin-note{
  padding:11px 13px; border-radius:10px; margin:10px 0;
  background:color-mix(in srgb, var(--accent) 7%, transparent); border:1px solid var(--line);
  font-size:12px; line-height:1.7; color:var(--text-dim);
}
.admin-note strong{color:var(--text);}
.admin-note-warn{background:rgba(255,193,94,.09);}
.admin-contrib-json{
  max-height:220px; overflow:auto; margin:8px 0; padding:9px 11px;
  background:var(--search-bg); border:1px solid var(--line); border-radius:8px;
  font-size:11px; line-height:1.5; white-space:pre-wrap; word-break:break-word;
}

.admin-table{width:100%; border-collapse:collapse; font-size:12.5px; margin-bottom:6px;}
.admin-table th{
  text-align:start; font-size:10.5px; text-transform:uppercase; letter-spacing:.05em;
  color:var(--text-dim); padding:6px 8px; border-bottom:1px solid var(--line);
}
.admin-table td{padding:7px 8px; border-bottom:1px solid var(--line); vertical-align:middle;}
.admin-table input, .admin-table select{width:100%; min-width:60px;}
.admin-course-table td:nth-child(4) input{max-width:64px;}
.admin-mini{padding:4px 9px; font-size:11px;}
.admin-danger{border-color:var(--prereq); color:var(--prereq);}
.admin-pill{
  display:inline-block; padding:2px 8px; border-radius:99px; font-size:10.5px; font-weight:800;
  background:rgba(255,255,255,.06); color:var(--text-dim);
}
.admin-pill-on{background:rgba(93,255,160,.14); color:var(--unlock);}

.admin-editor{
  margin-top:18px; padding:16px; border-radius:12px;
  background:var(--panel); border:1px solid var(--accent);
}
.admin-row{display:flex; gap:6px; align-items:center; margin-bottom:6px; flex-wrap:wrap;}
.admin-row input, .admin-row select{flex:1 1 120px; min-width:0;}
.admin-check{display:flex; align-items:center; gap:6px; font-size:12px; color:var(--text-dim); margin:8px 0;}
.admin-check input{width:auto;}
.admin-savebar{align-items:center; gap:10px; flex-wrap:wrap;}
.admin-savebar .admin-hint{margin:0;}

/* The app themes .form-field inputs, but the admin tables and rows are neither
   — so every input inside a course row rendered as a raw browser control: white
   box, white dropdown, on a dark panel. Styling them one selector at a time is
   how that happened in the first place, so this covers the whole overlay and
   anything added to it later inherits it. */
#adminOverlay input[type="text"],
#adminOverlay input[type="number"],
#adminOverlay input[type="search"],
#adminOverlay input[type="password"],
#adminOverlay select,
#adminOverlay textarea{
  background:var(--search-bg); border:1px solid var(--line);
  border-radius:8px; padding:8px 10px; color:var(--text);
  font-size:13px; font-family:inherit; box-sizing:border-box;
}
#adminOverlay textarea{resize:vertical; min-height:70px;}
#adminOverlay input:focus,
#adminOverlay select:focus,
#adminOverlay textarea:focus{
  outline:none; border-color:var(--accent); box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);
}
/* Dropdown lists are painted by the OS, which does not read the page's theme —
   naming both sides keeps an open <select> from flashing white. */
#adminOverlay select option{background:var(--panel); color:var(--text);}
#adminOverlay input::placeholder{color:var(--text-dim); opacity:.7;}
/* Table cells are tight; the overlay padding above is too generous for them. */
#adminOverlay .admin-table input,
#adminOverlay .admin-table select{padding:6px 8px; font-size:12.5px;}

/* Faculty and term groups. Both exist so structure is visible rather than
   implied: a major sits inside a faculty, a course sits inside a term, and a
   flat list said neither. The orphan variant is deliberately loud — it marks
   the cases students cannot reach at all. */
.admin-facgroup, .admin-termgroup{
  margin:14px 0; padding:12px 14px; border-radius:12px;
  background:var(--panel); border:1px solid var(--line);
}
.admin-facgroup.is-orphan, .admin-termgroup.is-orphan{
  border-color:var(--danger, #ff6b6b);
  background:rgba(255,107,107,.06);
}
.admin-facgroup-head{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:8px;}
.admin-facgroup-head h3{margin:0; font-size:14px;}
.admin-facgroup-head > div{flex:1 1 180px; min-width:0;}
.admin-facgroup-mark{display:flex; align-items:center; justify-content:center; width:30px; height:30px; flex:0 0 auto;}
.admin-facgroup .admin-addhere{flex:0 0 auto;}
.admin-facgroup-empty{margin:2px 0 0; font-style:italic;}
.admin-termgroup h4{margin:0; font-size:13px;}
.admin-termgroup-head{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:8px;}
.admin-termgroup-head h4{flex:1 1 auto;}

/* Picking an existing course rather than retyping one. Collapsed by default —
   it is an occasional action sitting above a list people scroll constantly. */
.admin-picker{
  margin:12px 0; border:1px solid var(--line); border-radius:12px;
  background:var(--panel); overflow:hidden;
}
.admin-picker > summary{
  cursor:pointer; padding:10px 14px; font-size:13px; font-weight:700; color:var(--text);
  list-style:none; user-select:none;
}
.admin-picker > summary::-webkit-details-marker{display:none;}
.admin-picker > summary:hover{background:rgba(255,255,255,.04);}
.admin-picker[open] > summary{border-bottom:1px solid var(--line);}
.admin-picker-body{padding:12px 14px;}
.admin-picker-results{max-height:320px; overflow-y:auto; margin-top:8px;}
.admin-pickhit{
  display:flex; align-items:center; gap:10px; width:100%; text-align:left;
  padding:8px 10px; margin-bottom:4px; border-radius:8px; cursor:pointer;
  background:rgba(255,255,255,.03); border:1px solid transparent; color:var(--text);
  font-family:inherit; font-size:12.5px;
}
.admin-pickhit:hover{border-color:var(--accent); background:color-mix(in srgb, var(--accent) 10%, transparent);}
.admin-pickhit-code{
  flex:0 0 auto; min-width:70px; font-variant-numeric:tabular-nums;
  font-weight:700; color:var(--text-dim);
}
.admin-pickhit-name{flex:1 1 auto; min-width:0;}

/* A row filling itself in is a big change to make silently — four boxes move
   at once. The flash says which row it was. */
@keyframes adminAutofill{from{background:color-mix(in srgb, var(--accent) 22%, transparent);} to{background:transparent;}}
.admin-table tr.is-autofilled td{animation:adminAutofill 1.2s ease-out;}
.admin-warn{color:var(--danger, #ff6b6b);}

/* Which university, which faculty, which major — the three sections that can
   write to a plan are otherwise reachable with none of that on screen. */
.admin-crumbs{
  display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  font-size:12px; color:var(--text-dim); margin:-4px 0 10px;
  padding:6px 10px; border-radius:8px; background:rgba(255,255,255,.03);
}
.admin-crumbs strong{color:var(--text);}
.admin-crumb-sep{opacity:.5;}

.admin-semlist{list-style:none; margin:0 0 10px; padding:0;}
.admin-semlist li{
  display:flex; align-items:center; gap:8px; justify-content:space-between;
  padding:5px 8px; border-radius:8px; background:rgba(255,255,255,.03); margin-bottom:3px; font-size:12.5px;
}
.admin-semlist select{max-width:160px;}

.admin-iconpick{display:flex; flex-wrap:wrap; gap:6px; margin-top:4px;}
.admin-icontile{
  width:38px; height:38px; display:flex; align-items:center; justify-content:center;
  border-radius:9px; border:1px solid var(--line); background:var(--panel);
  color:var(--accent); cursor:pointer; font-size:10px; padding:0;
}
.admin-icontile:hover{border-color:var(--accent);}
.admin-icontile.is-active{border-color:var(--accent); background:color-mix(in srgb, var(--accent) 18%, transparent);}

.admin-logo-preview{
  margin-top:8px; padding:10px; border-radius:9px; display:inline-flex;
  background:rgba(255,255,255,.05); border:1px solid var(--line);
}
.admin-assets{display:flex; flex-wrap:wrap; gap:10px;}
.admin-asset{
  width:150px; padding:9px; border-radius:10px; text-align:center;
  background:var(--panel); border:1px solid var(--line);
}
.admin-asset img{width:100%; height:70px; object-fit:contain; margin-bottom:6px;}
.admin-asset code{display:block; font-size:9.5px; color:var(--text-dim); word-break:break-all; margin-bottom:6px;}

@media (max-width:760px){
  .admin-body{flex-direction:column;}
  .admin-nav{
    flex:0 0 auto; width:100%; flex-direction:row;
    overflow-x:auto; overflow-y:visible;
    border-inline-end:none; border-bottom:1px solid var(--line);
  }
  .admin-navbtn{white-space:nowrap;}
  .admin-nav-context{display:none;}
  .admin-main{padding:14px;}
  .admin-table{display:block; overflow-x:auto;}
}

/* ==========================================================================
   SYNC CONSENT (js/30-sync.js) — shown when an official plan changed and the
   student has edits of their own on it. Deliberately blocking: it is the one
   moment the app would otherwise overwrite someone's work.
   ========================================================================== */
.sync-consent-overlay{
  position:fixed; inset:0; z-index:9700; display:none;
  align-items:center; justify-content:center; padding:18px;
  background:rgba(4,10,22,.72); backdrop-filter:blur(2px);
}
.sync-consent-overlay.open{display:flex;}
.sync-consent{
  width:min(520px,100%); max-height:82vh; overflow:auto;
  background:var(--panel); border:1px solid var(--accent); border-radius:14px; padding:18px;
}
.sync-consent h3{margin:0 0 8px; font-size:16px; line-height:1.35;}
.sync-consent-lead{font-size:12.5px; color:var(--text-dim); line-height:1.65; margin:0 0 12px;}
.sync-consent-list{list-style:none; margin:0 0 10px; padding:0;}
.sync-change{
  font-size:12.3px; line-height:1.55; padding:6px 10px; margin-bottom:4px;
  border-radius:8px; border-inline-start:3px solid var(--line);
  background:rgba(255,255,255,.035); color:var(--text);
}
/* Colour carries the same meaning it does everywhere else in the app, so the
   list is skimmable without a legend. */
.sync-change-add{border-inline-start-color:var(--unlock);}
.sync-change-remove{border-inline-start-color:var(--prereq);}
.sync-change-prereq{border-inline-start-color:var(--accent);}
.sync-change-move,.sync-change-change,.sync-change-rename,.sync-change-structure{border-inline-start-color:#ffc15e;}
.sync-consent-more{font-size:11.5px; color:var(--text-dim); margin:0 0 10px;}
.sync-consent-warn{
  font-size:11.8px; line-height:1.6; color:var(--text-dim);
  background:rgba(255,193,94,.09); border-radius:9px; padding:9px 11px; margin:0 0 14px;
}
.sync-consent-actions{display:flex; gap:8px; flex-wrap:wrap;}
.sync-consent-actions .home-btn{flex:1 1 190px;}
.sync-consent-apply{border-color:var(--unlock); color:var(--text);}

/* ==========================================================================
   DIALOG HEIGHT — every .modal-card is capped to the viewport and scrolls
   inside itself.
   Nothing here limited a dialog's height, so a long one (the Developer Panel
   with 23 imported plans, the Degree Audit on a big major) simply grew past
   the bottom of the screen. The page itself does not scroll while a modal is
   open, so the ✕ and the buttons at the end became unreachable — the only way
   to see them was to zoom the browser out.

   All five .modal-card instances are <close><body>, so capping the card and
   scrolling the body keeps ✕ pinned and always clickable.
   ========================================================================== */
.modal-card{
  /* dvh, not vh: on a phone, vh counts the space behind the browser's own
     toolbar, which is exactly where a dialog's last button ends up. */
  max-height:calc(100dvh - 40px);
  display:flex;
  flex-direction:column;
}
.modal-card > .modal-body{
  overflow-y:auto;
  min-height:0;              /* or flex refuses to shrink it and nothing scrolls */
  overscroll-behavior:contain;/* stops the page behind from scrolling at the end */
  padding-inline-end:2px;    /* breathing room beside the scrollbar */
}
/* The close button stays pinned because the card no longer scrolls — but it
   must sit above the scrolled content, not under it. */
.modal-card > .modal-close{z-index:2;}

/* A long list inside a dialog gets its own bound too, so one runaway section
   cannot push everything else out of reach. */
#devPlanList,
#devUniList{
  max-height:min(42dvh, 420px);
  overflow-y:auto;
  overscroll-behavior:contain;
  padding-inline-end:2px;
  border-radius:8px;
}

/* The unscheduled-elective block (js/28-imported.js). Visually separated from
   the year blocks because these courses are a menu, not a schedule. */
.imp-elective-block{border-style:dashed;}
.imp-elective-note{
  font-size:11.5px; color:var(--text-dim); line-height:1.65; margin:0 0 10px;
}
.imp-elective-count{
  display:inline-block; margin-inline-start:6px; padding:1px 7px;
  border-radius:99px; background:color-mix(in srgb, var(--accent) 16%, transparent);
  font-size:10.5px; font-weight:800; color:var(--accent);
}
/* "(Optional)" next to an elective heading — the university's own app marks
   Spec. Elec. and Univ. Elec. that way, and the distinction matters: these are
   a menu you pick from, not courses the plan requires in a fixed term. */
.imp-optional-tag{
  font-size:11.5px; font-weight:700; color:var(--text-dim); opacity:.9;
}

/* Hours in a semester, and the note when this plan's own median says the term
   is a course heavier than the rest of it (js/28-imported.js). Warn-coloured,
   not error-coloured: a heavy term is a thing to notice, not a thing that is
   wrong. */
.imp-semester-title{display:flex; align-items:baseline; gap:8px;}

/* ============================================================
   THE PLAN SURFACE — eight changes, all on the course card and
   the semester it sits in.
   ============================================================ */

/* 35 · THE SEMESTER HEADER STICKS.
   Six courses in a two-column grid is three rows, and by the second one the
   header has scrolled away — taking the running hours total with it, which is
   the number you are checking WHILE you look at the courses. It rides just
   under the plan's own sticky progress meter. */
.imp-semester-title{
  position:sticky; top:var(--sticky-top, 0px); z-index:3;
  padding:6px 0 7px; margin-bottom:6px;
  background:linear-gradient(to bottom,
    var(--bg) 68%, color-mix(in srgb, var(--bg) 0%, transparent));
}

/* 1 · THE LOAD IS A VERDICT, NOT A NUMBER.
   Measured against this plan's own median term (js/28-imported.js), because a
   163-hour engineering degree and a 33-hour diploma do not share a definition
   of heavy. The hours stay next to it for anyone who wants them. */
.imp-sem-verdict{
  font-size:10px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  padding:2px 7px; border-radius:5px; line-height:1.5;
}
.imp-sem-verdict.is-light{background:var(--sem-light-bg,rgba(90,150,220,.16)); color:var(--sem-light,#7fb3e8);}
.imp-sem-verdict.is-balanced{background:var(--sem-ok-bg,rgba(70,190,130,.15)); color:var(--sem-ok,#4ecb8b);}
.imp-sem-verdict.is-heavy{background:var(--sem-heavy-bg,rgba(230,110,140,.16)); color:var(--sem-heavy,#ff7a9c);}

/* 18 · A THREE-LETTER BUCKET CODE.
   The plan encodes six requirement buckets in colour alone. Roughly one man in
   twelve cannot separate the green from the orange, and nobody can in a
   black-and-white printout. The code sits in the corner the checkbox does not
   use, quiet enough to ignore and legible enough to settle an argument. */
.course .req-code{
  position:absolute; top:7px; inset-inline-start:9px;
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:8.5px; font-weight:700; letter-spacing:.08em;
  color:var(--text-dim); opacity:.62; pointer-events:none;
}
.course{position:relative;}
.course .name{padding-top:11px;}

/* 43 · THE OTHER LANGUAGE, UNDERNEATH.
   Lectures, slides and the registrar all use the English title, so a student
   reading Arabic still has to match the card to what the portal calls it. */
.course .name-alt{
  font-size:10.5px; font-weight:500; line-height:1.35;
  color:var(--text-dim); opacity:.72; margin-top:1px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* 37 · SHAPE, NOT ONLY COLOUR, FOR THE THREE STATES.
   Solid fill for done, a full outline for open, a dashed outline for locked —
   readable in sunlight, in greyscale, and without colour vision. */
.course{border:1px solid transparent;}
.course.available:not(.completed){border-style:solid;}
.course:not(.available):not(.completed){
  border-style:dashed; border-color:var(--line);
}

/* 32 · THE GRADE COLOURS THE CARD.
   Once a grade is in, the bucket has done its job — you already know Calculus
   is a college requirement. What you want to see across a finished year is how
   it went. The stripe on the leading edge switches to the grade band; the
   bucket code above still says which block it belonged to. */
.course.completed.grade-a{--card-grade:#2f9e68; --card-grade-tx:#effaf4;}
.course.completed.grade-b{--card-grade:#3f6fd8; --card-grade-tx:#eef3ff;}
.course.completed.grade-c{--card-grade:#c08420; --card-grade-tx:#fff8ea;}
.course.completed.grade-d{--card-grade:#c4611f; --card-grade-tx:#fff2e8;}
.course.completed.grade-f{--card-grade:#c8365c; --card-grade-tx:#ffeef3;}
/* The card is FILLED by its requirement bucket, so an edge stripe would be
   invisible against it — the fill itself has to carry the grade. The bucket is
   not lost: the three-letter code added above still says which block the course
   belongs to, which is the whole reason those two changes ship together. */
.course.completed[class*=" grade-"]{
  background:var(--card-grade) !important;
  color:var(--card-grade-tx) !important;
}
.course.completed[class*=" grade-"] .course-meta,
.course.completed[class*=" grade-"] .name-alt,
.course.completed[class*=" grade-"] .req-code{
  color:var(--card-grade-tx); opacity:.82;
}

/* 36 · DENSITY, FOLDED INTO THE SIZE CONTROL THAT ALREADY EXISTS.
   Settings already offers Compact / Normal / Large / Extra large, and it only
   scaled the type. A second "density" control beside it would have been two
   settings for one decision. So Compact now means a genuinely tighter layout —
   closer gaps, no second-language line, smaller meta — which is what someone
   choosing it actually wants: a 163-hour engineering degree, one year at a
   time, without scrolling. js/17-theme.js writes data-card-size on the root. */
:root[data-card-size="compact"] .course-row{gap:6px;}
:root[data-card-size="compact"] .course .name-alt{display:none;}
:root[data-card-size="compact"] .course-meta{font-size:9.5px;}
:root[data-card-size="compact"] .imp-semester-block{margin-bottom:10px;}
:root[data-card-size="compact"] .imp-semester-title{padding:4px 0 5px;}
/* …and the two large steps go the other way: more air, not just bigger type. */
:root[data-card-size="large"] .course-row,
:root[data-card-size="xlarge"] .course-row{gap:12px;}
:root[data-card-size="xlarge"] .imp-semester-block{margin-bottom:22px;}

/* 31 · THE AUDIT TABLE SAYS IT SCROLLS.
   It always did — .audit-table-wrap has overflow-x:auto — but nothing on
   screen suggested it, so a table with two columns past the edge read as
   broken rather than scrollable. A fade on the trailing edge and a chevron,
   both of which disappear once you reach the end. */
.audit-table-wrap{position:relative;}
.audit-table-wrap::after{
  content:'\203A'; position:absolute; top:0; bottom:0; inset-inline-end:0;
  width:34px; display:flex; align-items:center; justify-content:center;
  font-size:17px; color:var(--accent); pointer-events:none;
  background:linear-gradient(to var(--fade-to,left),
    var(--card-bg) 30%, color-mix(in srgb, var(--card-bg) 0%, transparent));
  opacity:1; transition:opacity .18s;
}
.audit-table-wrap[dir="rtl"], [dir="rtl"] .audit-table-wrap{--fade-to:right;}
.audit-table-wrap.at-end::after{opacity:0;}
.audit-table-wrap:not(.can-scroll)::after{content:none;}
.imp-sem-hours{
  margin-inline-start:auto; font-variant-numeric:tabular-nums;
  font-weight:800; color:var(--text-dim); letter-spacing:0;
}
.imp-semester-block.is-heavy .imp-sem-hours{color:var(--warn);}
.imp-sem-heavy{
  margin:-4px 0 10px; font-size:11px; font-weight:700; line-height:1.45;
  color:var(--warn); text-transform:none; letter-spacing:0;
}

/* The zero-hour requirements, pinned above Year 1. Same block shell as a year
   so it reads as part of the plan rather than an annotation on it. */
.imp-pinned-block{border-style:dashed;}
.imp-pinned-block .imp-year-header{margin-bottom:2px;}

/* The Worker-address escape hatch on the admin sign-in screen. Collapsed until
   something fails, because it is a repair tool, not a normal step. */
.admin-endpoint{
  margin-top:18px; border-top:1px solid var(--line); padding-top:12px;
}
.admin-endpoint summary{
  cursor:pointer; font-size:12px; font-weight:700; color:var(--text-dim); list-style:revert;
}
.admin-endpoint summary:hover{color:var(--text);}
.admin-endpoint code{
  background:color-mix(in srgb, var(--accent) 10%, transparent); padding:1px 5px; border-radius:5px;
  font-size:11.5px; word-break:break-all;
}

/* ---- Admin: which mark actually wins -------------------------------------
   Three fields (uploaded image, built-in icon, emoji) produce one mark, and
   before this nothing showed which. Clearing the icon and setting a logo left
   the old emoji on screen with no explanation. */
.admin-markpreview{
  display:flex; align-items:center; gap:14px; margin:6px 0 16px;
  padding:12px 14px; border-radius:11px;
  background:color-mix(in srgb, var(--accent) 7%, transparent); border:1px solid var(--line);
}
.admin-markpreview-box{
  flex:0 0 auto; width:64px; height:64px; border-radius:12px;
  display:flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,.05); border:1px solid var(--line);
}
.admin-markpreview-title{font-size:12px; font-weight:800; margin-bottom:2px;}

/* ---- Admin: faculty rows ------------------------------------------------
   Four unlabelled boxes in a row gave no clue which was which — and the id,
   the one that must not change, looked like just another text field. */
.admin-faculty{
  border:1px solid var(--line); border-radius:11px;
  padding:11px 13px; margin-bottom:9px; background:rgba(255,255,255,.025);
}
.admin-faculty-head{display:flex; align-items:center; gap:9px; margin-bottom:9px;}
.admin-faculty-n{
  flex:0 0 auto; width:21px; height:21px; border-radius:50%;
  background:color-mix(in srgb, var(--accent) 18%, transparent); color:var(--accent);
  font-size:11px; font-weight:800; display:flex; align-items:center; justify-content:center;
}
.admin-faculty-name{flex:1 1 auto; font-size:13px; min-width:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.admin-faculty-grid{display:grid; grid-template-columns:1fr 1fr auto; gap:8px;}
.admin-faculty-grid label{
  display:flex; flex-direction:column; gap:3px;
  font-size:11px; font-weight:700; color:var(--text-dim); margin:0;
}
.admin-faculty-grid label input{width:100%;}
.admin-faculty-grid label:nth-child(3){width:78px;}
/* The id spans the full width and reads as the warning it is. */
.admin-faculty-id{grid-column:1 / -1;}
.admin-faculty-id span{font-weight:600; color:var(--prereq); text-transform:none; letter-spacing:0;}
.admin-faculty-id input{font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12.5px;}

/* With 77 icons the picker needs a bound, or it pushes the Save button off
   the screen on the very form it belongs to. */
.admin-iconpick{max-height:186px; overflow-y:auto; padding:4px; border-radius:9px;
  background:rgba(0,0,0,.14); overscroll-behavior:contain;}

@media (max-width:620px){
  .admin-faculty-grid{grid-template-columns:1fr;}
  .admin-faculty-grid label:nth-child(3){width:100%;}
  .admin-markpreview{flex-direction:column; align-items:flex-start;}
}

/* A university's description, set in Admin Mode. Sits under the Arabic name on
   its home tile; hidden entirely when empty so nothing shifts for a university
   that has none. */
.plan-card .pc-uni-desc{
  font-size:12px; line-height:1.6; color:var(--text-dim);
  margin:6px 0 0; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical;
  overflow:hidden;
}

/* ============================================================================
   CLOUD SYNC — the signed-in status card (js/52-cloud.js sectionHtml)

   Signed in used to render exactly like signed out: same heading, one dim
   grey line, same button. Nothing on screen said the thing a student signed
   in FOR had happened. This is the confirmation — who is signed in, that
   the work is off this device, and when it last went up.
   ========================================================================= */
.cloud-live{
  display:flex; align-items:center; gap:13px;
  padding:13px 15px; margin:8px 0 0;
  border-radius:14px;
  border:1px solid color-mix(in srgb, var(--unlock) 40%, transparent);
  background:color-mix(in srgb, var(--unlock) 11%, transparent);
}
.cloud-live-avatar{
  position:relative; flex:0 0 auto;
  width:42px; height:42px; border-radius:50%;
  background:var(--accent); color:var(--on-accent);
  display:flex; align-items:center; justify-content:center;
  font-size:18px; font-weight:800; line-height:1;
}
.cloud-live-tick{
  position:absolute; right:-3px; bottom:-3px;
  width:18px; height:18px; border-radius:50%;
  background:var(--unlock); color:var(--unlock-tx);
  border:2px solid var(--panel);
  display:flex; align-items:center; justify-content:center;
  font-size:9px; font-weight:800; line-height:1;
}
.cloud-live-body{display:flex; flex-direction:column; gap:2px; min-width:0;}
.cloud-live-name{
  font-size:14px; font-weight:800; color:var(--text);
  line-height:1.3; overflow-wrap:anywhere;
}
.cloud-live-sub{font-size:11.5px; color:var(--text-dim); line-height:1.4;}
.cloud-live-when{
  font-size:10.5px; font-weight:700; color:var(--unlock);
  letter-spacing:.02em; font-variant-numeric:tabular-nums;
}

/* ============================================================================
   ABOUT US  (js/75-about.js)

   Replaces three lines of prose that used to sit under every screen of the
   plan picker. One button, one page.
   ========================================================================= */
.ab-lead{font-size:13px; line-height:1.65; color:var(--text-dim); margin:0 0 12px;}
.ab-flag{
  display:flex; align-items:center; gap:9px; margin:0 0 18px;
  font-size:12.5px; color:var(--text-dim); line-height:1.5;
}
.ab-flag-mark{font-size:17px; flex:none; line-height:1;}
.ab-rows{display:flex; flex-direction:column; gap:2px;}
.ab-row{
  display:flex; align-items:flex-start; gap:12px;
  padding:12px 4px; border-top:1px solid var(--line);
}
.ab-row-ic{
  flex:none; width:30px; height:30px; border-radius:9px;
  background:color-mix(in srgb, var(--accent) 13%, transparent); color:var(--accent);
  display:flex; align-items:center; justify-content:center;
}
.ab-row-body{display:flex; flex-direction:column; gap:3px; min-width:0;}
.ab-row-label{
  font-size:10px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
  color:var(--text-dim);
}
.ab-row-value{font-size:13.5px; line-height:1.5; color:var(--text); overflow-wrap:anywhere;}
.ab-row-value a{color:var(--accent); text-decoration:none; font-weight:700;}
.ab-row-value a:hover{text-decoration:underline;}
/* A fax number sits under its campus switchboard: same row, quieter, and
   never a link, because nothing dials a fax. */
.ab-sub{font-size:12px; color:var(--muted); font-weight:600;}
.ab-row-value code{
  font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12.5px;
  background:var(--search-bg); border-radius:6px; padding:2px 7px;
}
.ab-more{margin-top:16px;}

/* ============================================================================
   COURSE DETAIL PANEL  (js/49-course-detail.js)

   Written narrow-first. Every rule here is the phone layout; the two-column
   arrangement only appears above 720px. That order is deliberate — the wide
   version is the exception in this app, not the baseline, and building it the
   other way is how a phone ends up with a desktop layout squeezed into it.
   ========================================================================= */
.modal-card.cd-card{max-width:840px; padding:0; overflow:hidden;}
.modal-card.cd-card .modal-body{padding:0;}
.modal-card.cd-card .modal-close{top:14px; right:14px; z-index:3;}
html[dir="rtl"] .modal-card.cd-card .modal-close,
.cd[dir="rtl"] ~ .modal-close{right:auto; left:14px;}

.cd-head{
  padding:20px 46px 16px 22px; border-bottom:1px solid var(--line);
  display:flex; gap:12px; align-items:flex-start; justify-content:space-between;
}
html[dir="rtl"] .cd-head, .cd[dir="rtl"] .cd-head{padding:20px 22px 16px 46px;}
.cd-title{min-width:0; padding-inline-end:26px;}
.cd-head h3{font-size:19px; font-weight:800; margin:0 0 5px; line-height:1.25;}
.cd-sub{font-size:11.5px; color:var(--text-dim); line-height:1.5;}

.cd-pill{
  flex:0 0 auto; font-size:11px; font-weight:800; border-radius:99px;
  padding:6px 13px; white-space:nowrap; border:1px solid transparent;
}
.cd-open{background:color-mix(in srgb, var(--accent) 14%, transparent); border-color:color-mix(in srgb, var(--accent) 45%, transparent); color:var(--accent);}
.cd-passed{background:rgba(47,191,113,.14); border-color:rgba(47,191,113,.45); color:var(--unlock);}
.cd-locked{background:rgba(255,182,72,.12); border-color:rgba(255,182,72,.4); color:#ffb648;}

.cd-body{display:block;}
.cd-main{padding:18px 22px;}
.cd-side{padding:4px 22px 20px;}

.cd-sec{margin-bottom:20px;}
.cd-sec:last-child{margin-bottom:0;}
.cd-lbl{
  font-size:10.5px; font-weight:800; letter-spacing:1.1px; text-transform:uppercase;
  color:var(--text-dim); margin-bottom:9px;
}
.cd-note{font-size:12.5px; color:var(--text); line-height:1.65; margin:0 0 10px;}
/* Inside the Details column it is a footnote on the row above it, not a
   paragraph of its own weight. */
.cd-side .cd-note{font-size:11px; color:var(--warn); margin:4px 0 0; line-height:1.5;}
.cd-note strong{font-weight:700;}

/* Idea 57 — the cost of dropping a course you have already passed
   (js/49-course-detail.js). It answers a question the plan never asked out
   loud, so it is set apart from the neighbouring sections by a warning rail
   rather than another uppercase label competing with them. */
.cd-drop{
  border-inline-start:3px solid var(--warn);
  padding-inline-start:10px;
}
.cd-drop .cd-note{margin-bottom:0;}

/* Each prerequisite carries its own state, so "why is this locked" is answered
   by looking rather than by reading. */
.cd-chain{display:flex; align-items:center; gap:7px; flex-wrap:wrap;}
.cd-chip{
  font-family:inherit; font-size:12px; font-weight:600; cursor:pointer;
  background:var(--search-bg); border:1px solid var(--line); color:var(--text);
  border-radius:9px; padding:7px 12px; max-width:100%; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap;
}
.cd-chip:hover{border-color:var(--accent);}
.cd-chip.is-ok{border-color:rgba(47,191,113,.5); color:var(--unlock);}
.cd-chip.is-missing{border-color:rgba(255,182,72,.5); color:#ffb648;}
.cd-chip.is-self{cursor:default; border-color:var(--accent); color:var(--accent); background:color-mix(in srgb, var(--accent) 10%, transparent);}
.cd-chip.is-self:hover{border-color:var(--accent);}
.cd-arrow{color:var(--text-dim); font-size:13px;}

.cd-row{
  display:flex; justify-content:space-between; align-items:center; gap:14px;
  padding:9px 0; border-bottom:1px solid var(--line); font-size:12.5px;
  color:var(--text-dim);
}
.cd-row:last-child{border-bottom:0;}
.cd-row b{color:var(--text); font-weight:700; text-align:end;}

/* The grade controls are js/21-course-modal-extras.js and are not restyled
   here — only given room and a rule off the section above them. */
.cd-extras:empty{display:none;}
.cd-extras{margin-top:18px; padding-top:16px; border-top:1px solid var(--line);}
.cd-extras .modal-extras{margin:0;}

/* ---- phone-only additions (mockup-matched): category chip, the three
   stat tiles, a compact always-visible prereq chain, and the why-locked/
   what-opens prose turned into two swipeable slides instead of stacking.
   Every rule below is hidden again inside the @media(min-width:720px)
   block further down — desktop keeps its original .cd-sub line, its
   stacked (non-swiping) .cd-sec pair, and none of this new markup. ---- */
.cd-catchip{
  display:inline-block; font-size:10.5px; font-weight:800; padding:4px 11px;
  border-radius:999px; margin-bottom:8px; background:rgba(0,0,0,.22);
}
.cd-catchip-core{color:var(--core); border:1px solid var(--core);}
.cd-catchip-skills{color:var(--skills); border:1px solid var(--skills);}
.cd-catchip-math{color:var(--math); border:1px solid var(--math);}
.cd-catchip-dept{color:var(--dept); border:1px solid var(--dept);}
.cd-catchip-eng{color:var(--eng); border:1px solid var(--eng);}
.cd-catchip-uni{color:var(--uni); border:1px solid var(--uni);}
.cd-catchip-free{color:var(--free); border:1px solid var(--free);}
.cd-catchip-misc{color:var(--misc); border:1px solid var(--misc);}
.cd-catchip-intern{color:var(--intern); border:1px solid var(--intern);}
.cd-catchip-summer{color:var(--summer); border:1px solid var(--summer);}

.cd-stats{display:grid; grid-template-columns:repeat(3,1fr); gap:8px; padding:0 22px 14px;}
.cd-stat{background:var(--search-bg); border-radius:12px; padding:10px 8px; text-align:center;}
.cd-stat-n{font-size:16px; font-weight:800; color:var(--text); line-height:1.2;}
.cd-stat-l{font-size:9.5px; color:var(--text-dim); font-weight:700; text-transform:uppercase; letter-spacing:.04em; margin-top:2px;}

.cd-mini-chain{display:flex; align-items:center; gap:6px; flex-wrap:wrap; padding:0 22px 14px;}
.cd-prereq-chip{
  font-family:inherit; font-size:11.5px; font-weight:700; cursor:pointer;
  background:var(--search-bg); border:1px solid var(--line); color:var(--text-dim);
  border-radius:999px; padding:6px 12px;
}
.cd-prereq-chip.is-ok{color:var(--unlock); border-color:rgba(47,191,113,.4);}
.cd-prereq-arrow{color:var(--text-dim); font-size:12px;}

.cd-slides{display:flex; overflow-x:auto; scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch;}
.cd-slides .cd-slide{flex:0 0 100%; scroll-snap-align:start; min-width:0;}
.cd-slides .cd-sec{margin-bottom:0;}
.cd-swipe-dots{display:flex; justify-content:center; gap:6px; margin-top:10px;}
.cd-swipe-dots span{width:6px; height:6px; border-radius:50%; background:var(--line); display:block;}
.cd-swipe-dots span.on{background:var(--accent);}

.cd-actions{padding:4px 22px 20px;}
.cd-action-primary{
  width:100%; padding:13px; border-radius:14px; border:none;
  background:var(--accent); color:var(--on-accent); font-family:inherit;
  font-size:13.5px; font-weight:800; cursor:pointer;
}

/* Set by js/49-course-detail.js's bind() on the underlying course row when
   "View in course tree" scrolls to it — a brief accent flash so the row
   that was just the whole sheet's subject is findable at a glance. */
.course.cd-highlight{animation:cd-highlight-pulse 1.6s ease;}
@keyframes cd-highlight-pulse{
  0%{box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 60%, transparent);}
  100%{box-shadow:0 4px 14px rgba(0,0,0,.22);}
}
@media (prefers-reduced-motion:reduce){ .course.cd-highlight{animation:none;} }

/* Planning status is meaningless for a course you can't act on yet, and
   the grade/difficulty/workload/notes block only makes sense once Done is
   picked — see js/21-course-modal-extras.js's own comments on is-locked/
   is-done for why the gating logic lives there, not here. */
.modal-extras.is-locked .cd-locked-hide{display:none;}
.modal-extras:not(.is-done) .cd-review-block{display:none;}

@media (min-width:720px){
  .cd-body{display:grid; grid-template-columns:1.35fr 1fr; align-items:start;}
  .cd-main{padding:20px 24px; border-inline-end:1px solid var(--line);}
  .cd-side{padding:20px 24px;}
  .cd-head{padding:22px 50px 18px 26px;}
  .cd-catchip, .cd-stats, .cd-mini-chain, .cd-swipe-dots, .cd-actions{display:none;}
  .cd-slides{display:block; overflow:visible;}
  .cd-slides .cd-slide{flex:none; width:auto;}
  .cd-slides .cd-slide:first-child .cd-sec{margin-bottom:20px;}
}

/* The 4 planning-status buttons become one segmented control (mockup-
   matched) on every platform, not a wrapped row of separate pills.
   Unconditional (not phone-scoped) — the user asked for this look on
   desktop too, not just the click behavior. Scoped to .cd-extras so
   .status-btn-group elsewhere (there is nowhere else — js/21-course-
   modal-extras.js is its only user — but scoping costs nothing and
   documents the intent) can never pick this up by accident. */
.cd-extras .status-btn-group{
  display:grid; grid-template-columns:repeat(4,1fr); gap:2px;
  background:var(--search-bg); border-radius:12px; padding:3px; margin-bottom:14px;
}
.cd-extras .status-btn{
  text-align:center; white-space:nowrap; border:none; overflow:hidden;
  text-overflow:ellipsis; min-width:0;
  border-radius:9px; padding:9px 4px; font-size:11px; background:none;
}
/* Equal specificity to the base .status-btn.active rule (two classes
   each), which sets this same background — restated here only because
   source order would otherwise let the plain .status-btn rule above win
   and leave the active segment's on-accent-colored text invisible
   against a transparent background. */
.cd-extras .status-btn.active{font-weight:800; background:var(--accent);}

/* The modal is the full height of a small screen, so the panel scrolls inside
   it rather than the page behind it. */
@media (max-width:719px){
  .modal-card.cd-card{max-height:90vh; display:flex; flex-direction:column;}
  .modal-card.cd-card .modal-body{overflow-y:auto; -webkit-overflow-scrolling:touch;}
  .cd-head{position:sticky; top:0; background:var(--panel); z-index:2;}
  .cd-head h3{font-size:17px;}
  .cd-arrow{transform:rotate(90deg); display:inline-block;}
  .cd-extras .status-btn{font-size:10.5px;}
}

/* ==========================================================================
   MODALS BECOME PAGES ON A PHONE
   On a desktop a dialog is a big popup over the app. On a phone that shape is
   wrong: measured on a 390px screen, every dialog rendered as a 350px card
   with 40px of the screen wasted down the sides, and My Path's content ran
   309px past its own card. A phone has no room to spare for a frame around a
   frame, so below 720px each dialog fills the screen like a page and gets a
   back arrow in a bar across the top — the same shape as the university's own
   app, and what a phone user expects to press.

   Deliberately last in this file, and written with three-class specificity,
   so it wins against the per-dialog sizing rules above (.modal-card.cd-card,
   .pg-modal-card, .audit-modal-card ...) without any of them needing to know
   this exists. Everything stays one .modal-overlay/.modal-card structure — no
   dialog needed new markup to take part.
   ========================================================================== */
@media (max-width:720px){
  .modal-overlay{
    padding:0;
    align-items:stretch; justify-content:stretch;
    /* Opaque: it is a page now, not something floating over the app. */
    background:var(--bg);
  }
  .modal-overlay .modal-card,
  .modal-overlay .modal-card.cd-card{
    width:100%; max-width:none;
    /* 100%, not 100dvh: the overlay itself (position:fixed; inset:0) is
       always exactly the real, current viewport with no unit of its own to
       go stale — inset re-resolves on every layout pass. 100dvh instead
       computes its own snapshot of the dynamic viewport, and on a scroll
       that hides the browser's address bar mid-gesture that snapshot can
       lag a frame behind the overlay around it, leaving the page short and
       whatever is behind it showing through the gap at the bottom until
       something (background/foreground, a resize) forces a fresh layout.
       Sizing from the parent's own already-live box sidesteps that
       entirely instead of chasing another dvh recalculation. */
    height:100%; max-height:100%;
    margin:0; border:0; border-radius:0; box-shadow:none;
    /* The 56px that used to clear a full-width back bar is gone with it —
       the exit is an edge rail now (see .modal-close below), which hangs off
       the side and reserves no row. The inline padding leaves the rail its
       30px plus a little air, so a heading never runs under it. Cards that
       carry their own .modal-body keep scrolling inside it; the ones that do
       not scroll here. */
    padding:16px 16px 24px;
    padding-inline-start:40px;
    overflow-y:auto; overscroll-behavior:contain;
  }
  /* The few cards that deliberately run edge-to-edge (course detail, the
     onboarding wizard) keep that, minus the space for the bar. */
  .modal-overlay .modal-card.cd-card,
  .modal-overlay .onboarding-modal-card{padding:0;}

  /* On a phone the onboarding card is the whole screen, so its footer belongs
     at the bottom of that screen rather than immediately under the last field
     — which left the Sign in button stranded halfway up with nothing below it.
     The chain has to reach #onboardingWizardBody: the card's only child is the
     modal body, and everything the step renders lives inside THAT. */
  .modal-overlay .onboarding-modal-card{
    display:flex; flex-direction:column; min-height:100%;
  }
  .modal-overlay .onboarding-modal-card > #onboardingWizardBody{
    display:flex; flex-direction:column; flex:1 1 auto;
  }
  .modal-overlay #onboardingWizardBody > .wiz-body{flex:1 1 auto;}
  .modal-overlay #onboardingWizardBody > .wiz-landing{
    flex:1 1 auto; justify-content:center; padding-bottom:56px;
  }

  .modal-overlay .modal-card > .modal-body{
    max-height:none;          /* the card owns the height on a phone */
    padding-inline-end:0;
  }

  /* The existing ✕ becomes an EDGE RAIL: a slim vertical tab on the leading
     edge, vertically centred, with a back arrow on it.
     It was a full-width bar across the top. That worked, but it cost 56px of
     a phone screen on every dialog and sat at the far end of a thumb's reach
     on a tall phone — you had to shuffle the phone down your hand to close
     anything. A rail at the vertical middle of the leading edge is where the
     thumb already rests, is 28px wide instead of a whole row, and never
     covers content: it hangs off the side rather than sitting over the page.
     font-size:0 hides the ✕ glyph without every dialog's markup changing;
     the arrow comes from ::before below. */
  .modal-overlay .modal-card .modal-close,
  .modal-overlay .modal-card.cd-card .modal-close{
    position:fixed; inset:50% auto auto 0;
    transform:translateY(-50%);
    width:30px; height:82px;
    display:flex; align-items:center; justify-content:center;
    gap:0; padding:0; margin:0;
    background:color-mix(in srgb, var(--panel) 92%, transparent);
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
    border:1px solid var(--line); border-inline-start:0;
    border-radius:0 15px 15px 0;
    color:var(--text); font-size:0; z-index:60;
    box-shadow:4px 0 20px rgba(0,0,0,.34);
  }
  /* Mirrored for Arabic — js/60-backbar.js sets dir on the card, so the
     logical inset and the rounded corners both follow the reading order. */
  .modal-overlay .modal-card[dir="rtl"] .modal-close{
    inset:50% 0 auto auto;
    border-inline-start:1px solid var(--line); border-inline-end:0;
    border-radius:15px 0 0 15px;
    box-shadow:-4px 0 20px rgba(0,0,0,.34);
  }
  .modal-overlay .modal-card .modal-close:active{
    background:var(--search-bg);
  }
  /* The button already contains a drawn icon — index.html puts a
     data-app-icon="close" span in every one of them. font-size:0 above hides
     a text glyph but has no effect on an SVG that carries its own width and
     height, so adding a pseudo-element glyph on top rendered BOTH: an ✕ and
     an arrow in the same control, which read as two buttons doing the same
     thing. The drawn icon steps aside wherever a pseudo supplies the mark. */
  .modal-overlay .modal-card .modal-close svg,
  .modal-overlay .modal-card .modal-close [data-app-icon]{display:none;}
  .modal-overlay .modal-card .modal-close::before{
    content:'\2190'; font-size:21px; line-height:1; font-weight:400;
  }
  /* In Arabic the page reads right-to-left, so "back" points the other way. */
  .modal-overlay .modal-card[dir="rtl"] .modal-close::before{content:'\2192';}

  /* Course detail becomes a bottom sheet instead of a full page — the
     course tree stays visible (dimmed) behind it. Every other dialog above
     is deliberately still a full page; a "quick look at one thing" is the
     one case a phone actually expects to float over what you were doing
     rather than replace it outright. #impCourseModalOverlay's ID gives
     every rule here enough specificity to win against the three-class
     rules above without needing !important anywhere. */
  #impCourseModalOverlay{
    align-items:flex-end; justify-content:center;
    background:rgba(5,7,15,.6);
  }
  #impCourseModalOverlay .modal-card.cd-card{
    height:auto; max-height:82dvh;
    border-radius:16px 16px 0 0;
    padding:20px 16px calc(20px + env(safe-area-inset-bottom, 0px));
    position:relative;
  }
  /* A drag handle — decorative, no JS drag-to-dismiss behind it, but the
     one visual cue every phone user already reads as "this is a sheet,
     tapping outside or swiping it away closes it" without being told. */
  #impCourseModalOverlay .modal-card.cd-card::before{
    content:''; position:absolute; top:8px; left:50%; transform:translateX(-50%);
    width:36px; height:4px; border-radius:3px; background:var(--line);
  }
  /* The generic full-width back bar fixed to the viewport's top doesn't
     make sense floating above a sheet that no longer starts there — a
     small round ✕ in the sheet's own corner replaces it. */
  #impCourseModalOverlay .modal-card.cd-card .modal-close{
    position:absolute; inset:14px 14px auto auto;
    width:30px; height:30px; border-radius:50%;
    background:var(--search-bg); border:1px solid var(--line);
    display:flex; align-items:center; justify-content:center;
    font-size:0; z-index:5;
  }
  #impCourseModalOverlay .modal-card.cd-card .modal-close svg,
  #impCourseModalOverlay .modal-card.cd-card .modal-close [data-app-icon]{display:none;}
  #impCourseModalOverlay .modal-card.cd-card .modal-close::before{
    content:'\2715'; font-size:14px; font-weight:400;
  }
  html[dir="rtl"] #impCourseModalOverlay .modal-card.cd-card .modal-close{
    inset:14px auto auto 14px;
  }
}

/* Landscape split view — a phone turned sideways gets the course list and
   the detail sheet side by side, docked to the edge, instead of the bottom
   sheet above. The backdrop goes fully click-through (pointer-events:none,
   background transparent) so the course tree behind stays genuinely
   interactive rather than merely visible-but-dimmed; only the ✕ closes it
   here; tap-the-backdrop-to-dismiss doesn't apply once "the backdrop" is
   itself a working pane, not a stand-in for one. */
@media (max-width:900px) and (orientation:landscape){
  #impCourseModalOverlay{
    align-items:stretch; justify-content:flex-end;
    background:transparent; pointer-events:none;
  }
  #impCourseModalOverlay .modal-card.cd-card{
    /* 100%, not 100dvh — see the comment on the full-page rule above. */
    width:min(380px, 46vw); max-width:none; height:100%; max-height:100%;
    border-radius:0; margin:0; box-shadow:-16px 0 34px rgba(0,0,0,.45);
    pointer-events:auto;
  }
  #impCourseModalOverlay .modal-card.cd-card::before{display:none;}   /* no drag handle — this isn't a bottom sheet here */
}

/* A dialog whose whole job is to show a lot at once should use the room a
   desktop actually has. */
@media (min-width:721px){
  #roadmapModalOverlay .modal-card{max-width:1100px;}
  #achievementsModalOverlay .modal-card{max-width:900px;}
  #planOverviewOverlay .modal-card{max-width:1100px;}
}

/* "pick 2 of 4" beside an elective category — without it the capped hours
   figure looks like a mistake rather than the requirement it represents. */
.ro-bd-cat-pick{
  font-size:10.5px; font-weight:700; color:var(--text-dim);
  background:color-mix(in srgb, var(--accent) 14%, transparent); border-radius:99px; padding:1px 8px;
}

/* The floating assistant and Fix buttons sit above everything, which on a
   phone put them on top of a dialog that is now a full page. Hide them while
   one is open — the page has its own back arrow and does not need them.
   The "More" sheet isn't a .modal-overlay but now covers most of the
   screen too (it became a bottom sheet, not a corner-hugging drawer), so
   it needs the same treatment or the Fix button sits visibly on top of
   whatever row happens to land under it. */
@media (max-width:720px){
  body:has(.modal-overlay.open) .fix-launcher,
  body:has(.app-sidebar.open) .fix-launcher{display:none !important;}
}

/* ==========================================================================
   PHONE BOTTOM TAB BAR (js/37-sidebar.js) — Dashboard / Plan / Assistant /
   More, shown only on phone and only while the sidebar itself would be
   (body.has-sidebar drives both). Hidden outright above 720px: this is a
   phone-only affordance, the desktop sidebar is untouched.
   ========================================================================== */
.sb-tabbar{display:none;}
@media (max-width:720px){
  /* The bottom bar's own "More" tab opens this exact drawer, so the floating
     hamburger at the top-left is the same button twice on the same screen.
     The tab bar is the one a thumb can reach, so it is the one that stays. */
  body.has-sidebar .sb-toggle-btn{display:none;}

  /* Floating glass pill instead of a bar flush with the screen edge — the
     backdrop-filter blur is the point (content scrolling underneath stays
     legibly visible through it, the way a real app's translucent chrome
     does), rgba background is what's left if a browser doesn't support
     backdrop-filter at all, which still reads fine on its own. */
  body.has-sidebar .sb-tabbar{
    display:flex; position:fixed; left:14px; right:14px;
    bottom:calc(14px + env(safe-area-inset-bottom, 0px)); z-index:950;
    background:rgba(19,26,44,.72); backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
    border:1px solid rgba(255,255,255,.08); border-radius:26px;
    padding:7px 5px;
    box-shadow:0 14px 34px rgba(0,0,0,.4);
    transition:transform .2s ease;
  }
  /* A dialog is now a full page on phone (see the modal rules above) — its
     own back arrow is the way out, and the tab bar underneath it would just
     be a second, confusing way to leave. */
  body:has(.modal-overlay.open) .sb-tabbar{display:none;}
  /* Opening the sidebar itself (the "More" tab) as a drawer isn't a
     .modal-overlay, so the rule above doesn't catch it — without this the
     tab bar's own z-index (950, needed to sit above page content) also put
     it above the drawer, leaving it visibly poking through the drawer's
     bottom edge instead of getting out of its way. Slide it off the bottom
     edge instead of the drawer's own translateX so the two motions read as
     one panel swapping for another, not two unrelated things happening at
     once. */
  body:has(.app-sidebar.open) .sb-tabbar{transform:translateY(100%); pointer-events:none;}

  .sb-tab{
    flex:1; display:flex; flex-direction:column; align-items:center; gap:3px;
    background:none; border:none; padding:6px 2px; color:var(--text-dim);
    font-family:inherit; cursor:pointer; border-radius:18px;
    transition:color .15s ease;
  }
  /* The icon capsule, not the whole tab, carries the active state. A tinted
     rectangle behind the entire tab was the old treatment and it was barely
     visible — 16% of the accent over a translucent bar, which on a bright
     screen outdoors is no signal at all. A filled capsule under the icon is
     unmistakable at a glance and is the shape a phone tab bar is expected
     to use. */
  .sb-tab-ic{
    display:inline-flex; align-items:center; justify-content:center;
    width:46px; height:26px; border-radius:999px; position:relative;
    transition:background .18s ease, color .18s ease;
  }
  .sb-tab-lbl{font-size:9.5px; font-weight:700; transition:color .15s ease;}
  .sb-tab.active{color:var(--accent);}
  .sb-tab.active .sb-tab-ic{background:var(--accent); color:var(--on-accent);}
  .sb-tab.active .sb-tab-lbl{font-weight:800;}
  /* Tucked into the capsule's own top-right corner. At top:-6px/right:-14px
     it floated detached above the icon and reached into the neighbouring
     tab, so the Dashboard percentage read as if it belonged to Plan. */
  .sb-tab-badge{
    position:absolute; top:-4px; right:3px; font-size:8px; font-weight:800;
    color:var(--on-accent); background:var(--accent);
    border:1.5px solid var(--header-bg);
    border-radius:999px; padding:0 3px; line-height:1.5;
    white-space:nowrap; font-variant-numeric:tabular-nums;
  }
  /* On the active tab the capsule is already accent-filled, so an accent
     badge on top of it would disappear into its own background. */
  .sb-tab.active .sb-tab-badge{
    background:var(--on-accent); color:var(--accent); border-color:var(--accent);
  }

  /* Raised clear of the tab bar so neither floating button sits under it —
     a few px more than before since the bar itself now floats 14px off the
     bottom edge instead of sitting flush against it. */
  body.has-sidebar .fix-launcher{bottom:92px;}
  body.has-sidebar .asst-panel{bottom:154px;}
  body.has-sidebar .fix-panel{bottom:154px;}

  /* Every toast in the app sits at bottom:22px, which was fine when the
     bottom of the screen was empty. The floating tab bar occupies roughly
     14px–88px from the bottom edge, so a toast landed squarely inside it —
     "🔄 78 new." covered the Plan and Assistant tabs outright, and both
     stayed unreachable for as long as it showed.
     
     Clearing the bar alone is not enough: a toast is centred and up to the
     full width of the screen, and the two floating launchers sit at 92px,
     so it has to clear those too or .toast-action's own button ends up
     under one of them. The bottom of a phone screen now stacks in three
     bands — tab bar, launchers, toasts — none of them overlapping. */
  body.has-sidebar .toast-msg,
  body.has-sidebar .toast-action,
  body.has-sidebar .unlock-toast{bottom:152px;}
}

/* ---------- long-press quick actions (js/74-course-gestures.js) ----------
   Phone-only in practice (only ever opened from a touch long-press), but
   not media-query-gated itself — its position is computed in JS from the
   card's real on-screen rect, which already accounts for viewport width. */
/* A bottom sheet. This gesture only exists on phones, where a course card
   is a full-width row — there is no room beside one for a popover, which is
   why the old positioning ended up dropping the menu on top of the course it
   was acting on. */
/* Nearly clear. The dimming that matters happens on the plan itself (see
   .qa-dim below) so the courses this one connects to stay lit through it —
   an opaque sheet of dark over everything would bury the very thing the
   long-press just traced. This still catches the tap that dismisses. */
.course-qa-backdrop{
  position:fixed; inset:0; z-index:1499;
  background:rgba(3,6,14,.28);
  animation:qa-fade .16s ease-out;
}

/* Hold a course: the plan drops back and what it connects to stays lit —
   hold Calculus I and Calculus II is the thing still bright. .node-active is
   the same class the existing hold-to-trace already sets, so this reuses
   that walk over the prerequisite edges rather than repeating it. */
.qa-dim .course{
  opacity:.28; transition:opacity .2s ease, transform .2s ease;
}
.qa-dim .course.node-active,
.qa-dim .course.node-needs,
.qa-dim .course.node-unlocks,
.qa-dim .course.qa-subject{opacity:1;}
.qa-dim .course.qa-subject{
  transform:scale(1.02);
  box-shadow:0 0 0 2px var(--accent), 0 10px 26px rgba(0,0,0,.4);
}
/* Held: what the course NEEDS is ringed in the prerequisite colour, what it
   OPENS in the unlock colour, so the two directions are told apart at a
   glance rather than both reading as "connected". */
.qa-dim .course.node-needs:not(.qa-subject){
  box-shadow:0 0 0 2px var(--prereq), 0 8px 20px rgba(0,0,0,.34);
}
.qa-dim .course.node-unlocks:not(.qa-subject){
  box-shadow:0 0 0 2px var(--unlock), 0 8px 20px rgba(0,0,0,.34);
}
@media (prefers-reduced-motion:reduce){
  .qa-dim .course{transition:none;}
  .qa-dim .course.qa-subject{transform:none;}
}
.course-qa-menu{
  position:fixed; z-index:1500;
  left:10px; right:10px; bottom:calc(10px + env(safe-area-inset-bottom, 0px));
  background:var(--panel); border:1px solid var(--line); border-radius:18px;
  box-shadow:0 16px 40px rgba(0,0,0,.5); padding:6px; display:flex; flex-direction:column; gap:2px;
  animation:qa-rise .2s cubic-bezier(.2,.8,.3,1);
}
@keyframes qa-fade{from{opacity:0;} to{opacity:1;}}
@keyframes qa-rise{from{transform:translateY(14px); opacity:0;} to{transform:none; opacity:1;}}
@media (prefers-reduced-motion:reduce){
  .course-qa-backdrop, .course-qa-menu{animation:none;}
}

/* Says which course this is about — the card itself is behind the backdrop,
   and a sheet of three verbs with no subject is a guess. */
.course-qa-head{
  display:flex; flex-direction:column; align-items:center; gap:7px;
  padding:6px 12px 10px; border-bottom:1px solid var(--line); margin-bottom:4px;
}
.course-qa-grip{
  width:36px; height:4px; border-radius:999px; background:var(--line); flex:none;
}
.course-qa-title{
  font-size:12.5px; font-weight:800; color:var(--text-dim); text-align:center;
  line-height:1.35; overflow-wrap:anywhere;
}
.course-qa-menu button{
  display:flex; align-items:center; gap:12px; width:100%; text-align:left; padding:14px 14px; border-radius:12px;
  background:none; border:none; color:var(--text); font-family:inherit; font-size:14.5px; font-weight:600; cursor:pointer;
}
.course-qa-menu button .app-icon-svg{color:var(--text-dim); flex:none;}
.course-qa-menu button:hover,
.course-qa-menu button:focus-visible{background:var(--search-bg);}
.course-qa-menu button:active{background:var(--search-bg);}
[dir="rtl"] .course-qa-menu button{text-align:right;}

/* The tab bar's own z-index (950) is below the sheet, but it would still sit
   visible under the backdrop as if it were still usable. Same slide-away the
   More drawer already gets. */
body:has(.course-qa-backdrop) .sb-tabbar{transform:translateY(100%); pointer-events:none;}
body:has(.course-qa-backdrop) .fix-launcher{display:none;}

/* The pair wrapper only means something in the phone layout; everywhere else
   it must be invisible to layout so the existing grid is unchanged. */
.cp-pair{display:contents;}
.cp-pair-label{display:none;}

/* ==========================================================================
   PHONE STUDY PLAN — course cards become one-line rows
   A course was a fully category-coloured tile (green/blue/amber background
   filling the whole card) in a two-up wrapping grid — readable, but loud:
   every single card is shouting the same category information the year's
   own legend already states once. A row with a colour STRIPE says the same
   thing quietly, and switching from a 2-column grid to one column per row
   means a whole semester fits on screen without the "+N more" style
   scrolling a dense grid otherwise needs.

   Deliberately last in the file (same reasoning the phone-modal rules give
   for their own position): every rule below is a compound class selector
   (.course.core, .course.available, ...) at equal-or-higher specificity
   than what it overrides, so position mostly doesn't matter here — but the
   base .course flex-basis override needs to win against the existing
   @media(max-width:640px) .course{flex:1 1 44%} rule above at the same
   (0,1,0) specificity, which only source order decides.

   Every interaction this session already built on .course — swipe-to-mark
   (js/57-card-input.js), long-press quick actions (js/74-course-gestures.js),
   hold-to-trace prerequisite highlighting (js/28-imported.js) — reads the
   same DOM structure and class names untouched; nothing here adds or
   removes an element, only how the existing ones are painted. */
@media (max-width:720px){
  /* The prerequisite connector SVG draws a path between each course pair's
     real on-screen coordinates — which reads fine on desktop's wrapping
     grid, where prereqs sit at different x-positions. One column per row
     means every card shares the same x-center, so every edge collapses
     into the same straight vertical stroke: a hold-to-trace on a course
     with several links just paints one line down through everything,
     impossible to tell apart. Hiding the layer here doesn't lose the
     trace interaction itself — handleCourseHoverEnter() in
     js/28-imported.js still adds .node-active to every connected card in
     sequence, and that glow (below, already phone-visible) is what
     actually answers "what does this connect to" on a single column. */
  .connector-layer{display:none;}

  /* Two across, not one. The row treatment below is right — a colour stripe
     rather than a full tile — but a semester in a single column is a very
     long scroll, and this card's content (a name, one meta line, a code) is
     narrow enough to sit two-up on a phone. Below 360px it drops back to one
     column: two 150px cards there truncate names rather than fit them. */
  /* Back to stretch, now that every name is one line: with no two-line cards
     left there is nothing to be stretched TO, so the row comes out uniform
     AND short — which is what flex-start was working around when names could
     run to two lines. */
  .course-row{align-items:stretch; gap:8px;}

  /* A lecture and its lab: one registered course, two meetings. The pair
     takes a full row and a bracket is drawn down the margin spanning both,
     labelled — the two cards keep their own boxes and their own checkboxes,
     because a student really does attend them separately. */
  .cp-pair{
    position:relative; flex:1 1 100%; min-width:0;
    display:flex; flex-direction:column; gap:8px;
    padding-inline-end:24px;
  }
  .cp-pair::after{
    content:''; position:absolute; inset-inline-end:7px; top:6px; bottom:6px; width:9px;
    border:2px solid var(--accent); border-inline-start:none;
    border-start-end-radius:8px; border-end-end-radius:8px;
    opacity:.7;
  }
  .cp-pair > .course{flex:1 1 auto;}
  .cp-pair-label{
    display:block;
    position:absolute; inset-inline-end:-6px; top:50%;
    transform:translateY(-50%) rotate(90deg); transform-origin:center;
    font-size:8.5px; font-weight:800; letter-spacing:.07em; text-transform:uppercase;
    color:var(--accent); white-space:nowrap; pointer-events:none;
  }
  [dir="rtl"] .cp-pair-label{transform:translateY(-50%) rotate(-90deg);}
  /* A course removed from the plan (js/12-removed.js — placing out of
     Intermediate English, say) is display:none, so its pair wrapper collapses
     to zero height. The bracket and the rotated label are absolutely
     positioned, so they went on painting at that collapsed position — over
     whatever card happened to sit beside it, sometimes two labels on top of
     each other. Nothing to bracket, nothing drawn. */
  .cp-pair:not(:has(> .course:not(.course-removed))){display:none;}
  .cp-pair:not(:has(> .course:not(.course-removed) ~ .course:not(.course-removed)))::after,
  .cp-pair:not(:has(> .course:not(.course-removed) ~ .course:not(.course-removed))) > .cp-pair-label{display:none;}

  .course{
    flex:1 1 calc(50% - 4px); max-width:none; min-width:0; min-height:auto;
    padding:8px 11px 8px 11px;
  }
  @media (max-width:359px){
    .course{flex:1 1 100%;}
  }
  .plan-page.rtl-mode .course,
  .sheet.rtl-mode .course{padding:8px 11px 8px 11px;}

  /* Two-up means half the width, so the name gets two lines rather than
     being cut off at one, and the checkbox moves in to buy them back. */
  /* One line, ellipsised. Two-line names were what made the cards different
     heights and what made the tall ones tall — and a card in a grid is a
     label you scan, not the place you read a full course title. The whole
     name is still on the card's aria-label and in the detail panel a tap
     away, and the catalogue code underneath is unabbreviated. */
  .course .name{
    font-size:12.5px; padding:0; margin:0; line-height:1.25;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .plan-page.rtl-mode .course .name,
  .sheet.rtl-mode .course .name{padding:0;}
  .course-meta{font-size:10px; line-height:1.3; margin-top:2px;}
  /* The card sits inside a block headed with its year — repeating it here
     is what pushed this line onto a second row at half width. The card's
     aria-label still carries all three parts. */
  .course-meta .cm-year,
  .course-meta .cm-year + .cm-sep{display:none;}

  /* 18px at the top-right rather than 22px centred: two-up, a vertically
     centred circle sat in the middle of a two-line name. The 11px padding
     keeps the tap target at 40px either way. */
  /* Bottom-right, not top-right. Up beside the title it stole ~26px from
     the one line the name now gets, and seven names in a first semester were
     truncating because of it. Down here it sits level with the course code,
     which is short. */
  .course-check{
    top:auto; bottom:7px; right:7px; transform:none;
    width:16px; height:16px; padding:12px; margin:-12px;
  }
  .plan-page.rtl-mode .course-check,
  .sheet.rtl-mode .course-check{right:auto; left:7px; margin:-12px;}
  /* inset must match the 11px padding above — the padding is an invisible
     44px tap-target extension (existing convention, see the base rule's
     own comment above), not part of the drawn circle; inset:0 here would
     have drawn the circle across the whole padded box instead of just its
     22px content box, which is what actually happened the first time. */
  .course-check::before{inset:12px; background:var(--header-bg); border-color:var(--line);}
  .course-check{font-size:11px;}
  .course.completed .course-check::before{background:#2ecc71; border-color:#2ecc71;}
  .course.completed .course-check{color:var(--on-accent);}

  /* The pulsing available-glow was designed for a fully colour-filled
     card, where a thin accent ring was the only way to say "and this one
     you can register for right now" over the noise. Against the new flat
     panel it read as loud on every row — including already-completed
     ones, which also carry .available (see js/28-imported.js: the class
     is added for done OR available, not available alone) and have no
     reason to keep pulsing "you can take this" once it's finished. Kept
     only as a quiet static ring on courses that are genuinely open and
     not yet done; the elevation shadow already common to every row is
     what's left everywhere else. */
  .course.available{box-shadow:0 4px 14px rgba(0,0,0,.22); animation:none;}
  .course.available:not(.completed){
    box-shadow:0 4px 14px rgba(0,0,0,.22), 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
  }

  /* .node-active (the hold-to-trace highlight, js/28-imported.js) is the
     same (class,class) specificity as .course.available just above, and
     source order would otherwise let .available's plain shadow win on any
     course that's both available/completed AND being traced — silencing
     the one visual cue tracing has left now that the connector line itself
     is hidden on phone (see .connector-layer{display:none} above). Restated
     after, so it always wins the tie.

     Two colours, not one. This was a single hardcoded pink for everything a
     held course touched, which said "related" and nothing else — a student
     holding Calculus I could not tell which of the lit cards it NEEDS and
     which it OPENS. They are the plan's own prerequisite and unlock colours
     now, the same two the legend already names, and they come from the
     theme rather than from a literal. */
  .course.node-active{
    box-shadow:0 0 0 2px var(--accent), 0 0 16px color-mix(in srgb, var(--accent) 50%, transparent), 0 6px 14px rgba(0,0,0,.35);
  }
  .course.node-needs{
    box-shadow:0 0 0 2px var(--prereq), 0 0 16px rgba(255,111,184,.45), 0 6px 14px rgba(0,0,0,.35);
  }
  .course.node-unlocks{
    box-shadow:0 0 0 2px var(--unlock), 0 0 16px rgba(74,222,128,.45), 0 6px 14px rgba(0,0,0,.35);
  }
}

/* ==========================================================================
   PHONE COURSE LIBRARY — same elevation-list treatment as Contacts/course
   rows, applied to the plan-editor's course browser (js/28-imported.js
   openLibrary). .lib-row/.lib-list/.lib-add-btn exist so this can win
   against nothing else fighting for it — see the base rule's own comment. */
@media (max-width:720px){
  .lib-list{max-height:none; display:flex; flex-direction:column; gap:8px;}
  .lib-row{
    border-bottom:none; border-radius:12px; padding:11px 13px;
    background:var(--panel); box-shadow:0 4px 14px rgba(0,0,0,.22);
    font-size:13px;
  }
  .lib-add-btn{border-radius:999px; flex:0 0 auto;}

  /* On phone the rows are already elevated cards with gaps between them, so
     a shelf reads as a group without a rule under its header — the header
     just needs to sit clear of the card below it and stay put while the
     shelf scrolls. */
  .lib-shelf-open .lib-shelf-body{display:flex; flex-direction:column; gap:8px;}
  .lib-shelf-head{
    /* var(--panel), matching the modal card this scrolls inside — the sticky
       header has to be opaque to cover the rows passing under it, and any
       other value paints a visibly darker band across the list. */
    background:var(--panel); border-bottom:none;
    padding:8px 2px 6px; margin:0 0 2px;
  }
  .lib-shelf + .lib-shelf{margin-top:6px;}
}

/* ==========================================================================
   PHONE CONTACTS — a bordered auto-fill grid becomes one elevated list
   .ct-grid's auto-fill(minmax(230px,1fr)) already collapses to a single
   column under ~390px, so the layout was never broken on phone — but each
   .ct-card kept its desktop treatment (thin border, flat background), which
   reads as a form field repeated N times rather than a directory. Same
   elevation-over-border language as every other list in this redesign
   (course rows above, the More sheet's .sb-item rows); markup and js/68-
   contacts.js are untouched, only how the existing cards are painted. */
@media (max-width:720px){
  .ct-search{border-radius:14px; padding:11px 14px; box-shadow:0 2px 8px rgba(0,0,0,.18);}
  .ct-chips{flex-wrap:nowrap; overflow-x:auto; -webkit-overflow-scrolling:touch; padding-bottom:2px;}
  .ct-chip{flex:0 0 auto;}
  .ct-grid{display:flex; flex-direction:column; gap:8px;}
  /* var(--panel) matches the card this scrolls inside; a sticky header has
     to be opaque to cover the rows passing under it. */
  .ct-group-head{background:var(--panel); border-bottom:none; padding:8px 2px 6px; margin-bottom:2px;}
  .ct-group + .ct-group{margin-top:8px;}
  .ct-card{
    border:none; border-radius:14px; padding:13px 14px;
    box-shadow:0 4px 14px rgba(0,0,0,.22);
  }
  .ct-card-top{align-items:center;}
  .ct-avatar{
    width:40px; height:40px; flex:0 0 40px; font-size:18px;
    background:color-mix(in srgb, var(--accent) 14%, transparent); border:none; color:var(--accent);
  }
  .ct-name{font-size:14px;}
  .ct-email-row{margin-top:2px;}
  .ct-copy{padding:5px 12px;}
}

/* ==========================================================================
   PHONE PLAN MY NEXT SEMESTER — the pool/tray builder (js/25-advisor.js)
   loses its thin borders the same way every other list in this redesign
   did. Every selector is scoped under #advisorModalBody: .sb- here is a
   pre-existing, unrelated naming collision with the sidebar's own .sb-item/
   .sb-search (js/37-sidebar.js) — scoping is what keeps this from also
   repainting the "More" sheet. */
@media (max-width:720px){
  #advisorModalBody .sb-meter{border:none; border-radius:14px; box-shadow:0 4px 14px rgba(0,0,0,.22);}
  #advisorModalBody .sb-search{border-radius:12px; box-shadow:0 2px 8px rgba(0,0,0,.18);}
  #advisorModalBody .sb-row{border:none; border-radius:12px; box-shadow:0 4px 14px rgba(0,0,0,.22);}
  #advisorModalBody .sb-row:hover{box-shadow:0 4px 14px rgba(0,0,0,.22), 0 0 0 1px var(--accent);}
  #advisorModalBody .sb-tray{border:none; border-radius:14px; box-shadow:0 4px 14px rgba(0,0,0,.22);}
  #advisorModalBody .sb-tray-item{border:none; box-shadow:0 2px 8px rgba(0,0,0,.18);}
}

/* ==========================================================================
   PHONE SHARE THIS PLAN — same elevation swap for the link field and the
   QR card (js/72-share.js) as every other bordered box in this series. */
@media (max-width:720px){
  .share-link-input{border:none; border-radius:12px; box-shadow:0 2px 8px rgba(0,0,0,.18);}
  .share-qr-wrap{border:none; border-radius:14px; box-shadow:0 4px 14px rgba(0,0,0,.22);}
}

/* ==========================================================================
   ENGLISH PLACEMENT (js/77-english-level.js)

   The only dialog in the app with no way out but an answer, so it is styled
   to look like a decision rather than a notification: no ✕ in the corner to
   hunt for, four options at full width, each one a real target with the
   consequence written under it. */
.eng-overlay .eng-card{max-width:460px;}
/* Aligned to the first line, not to the middle of the block: this heading
   wraps to two lines on a phone, and a centred icon then floats out beside
   the second one with nothing next to the first. */
.eng-title{display:flex;align-items:flex-start;gap:9px;margin-top:0;}
.eng-title svg{flex:0 0 auto;margin-top:2px;}
.eng-lead{
  font-size:12.5px;line-height:1.6;color:var(--text-dim);
  margin:0 0 16px;
}
.eng-opts{display:flex;flex-direction:column;gap:9px;}
.eng-opt{
  display:flex;align-items:center;gap:12px;width:100%;
  padding:13px 14px;cursor:pointer;text-align:start;
  background:var(--search-bg);border:1px solid var(--line);border-radius:12px;
  color:var(--text);font-family:inherit;
  transition:border-color .15s ease, background .15s ease, transform .12s ease;
}
.eng-opt:hover{border-color:var(--accent);background:var(--panel);}
.eng-opt:active{transform:scale(.99);}
.eng-opt:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.eng-opt-body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto;}
.eng-opt-name{font-size:14px;font-weight:800;}
.eng-opt-sub{font-size:11.5px;color:var(--text-dim);font-weight:600;}
.eng-opt-chev{flex:0 0 auto;color:var(--text-dim);display:inline-flex;}
.eng-opt:hover .eng-opt-chev{color:var(--accent);}
[dir="rtl"] .eng-opt-chev svg{transform:scaleX(-1);}
@media (prefers-reduced-motion:reduce){
  .eng-opt{transition:none;}
  .eng-opt:active{transform:none;}
}

/* 52 · The same four options, inside the course popup instead of a dialog
   in front of the app. Tighter, because they are sharing a card with the
   course's own details rather than owning the screen. */
.eng-inline .eng-opts-inline{gap:6px;margin-top:8px;}
.eng-inline .eng-opts-inline .eng-opt{padding:9px 11px;}
.eng-inline .eng-opts-inline .eng-opt-name{font-size:12.5px;}
.eng-inline .eng-opts-inline .eng-opt-sub{font-size:10.5px;}

/* 41 · Reporting a wrong prerequisite from the course itself
   (js/86-prereq-report.js). A quiet row under the reasoning — it must be
   findable by someone who has just read an arrow they know is wrong, and
   invisible to everyone else, so it reads as a footnote rather than an
   action the course is asking for. */
.cd-report{
  display:flex; align-items:center; justify-content:space-between;
  gap:10px; flex-wrap:wrap;
  margin-top:14px; padding-top:11px; border-top:1px solid var(--line);
}
.cd-report-lbl{font-size:11.5px; color:var(--text-dim);}
.cd-report-btn{
  border:1px solid var(--line); background:transparent; color:var(--accent);
  font:inherit; font-size:11.5px; font-weight:700; cursor:pointer;
  padding:5px 11px; border-radius:999px;
}
.cd-report-btn:hover{border-color:var(--accent); background:var(--panel);}
.cd-report-btn:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
.cd-report-done{
  display:inline-flex; align-items:center; gap:5px;
  font-size:11.5px; font-weight:700; color:var(--skills);
}

/* The report form itself. One radio per listed prerequisite — a list short
   enough (rarely more than three) that radios beat a dropdown. */
.pr-course{
  margin:0 0 10px; font-size:13.5px; font-weight:800;
}
.pr-opts{display:flex; flex-direction:column; gap:7px; margin-bottom:14px;}
.pr-opt{
  display:flex; align-items:center; gap:9px;
  padding:9px 11px; border:1px solid var(--line); border-radius:10px;
  cursor:pointer; font-size:12.5px;
}
.pr-opt:hover{border-color:var(--accent);}
.pr-opt input{flex:0 0 auto; accent-color:var(--accent);}
.pr-opt-name{flex:1 1 auto; min-width:0; font-weight:700;}
.pr-opt-num{
  flex:0 0 auto; font-size:11px; color:var(--text-dim);
  font-variant-numeric:tabular-nums;
}
.pr-note-lbl{
  display:block; margin-bottom:6px;
  font-size:10.5px; font-weight:800; letter-spacing:1.1px;
  text-transform:uppercase; color:var(--text-dim);
}
/* Send is the one thing this dialog exists to do, so it is the filled one —
   same treatment .fix-btn-primary gives the equivalent button elsewhere. */
.pr-send{
  background:var(--accent); color:var(--on-accent); border-color:transparent;
  font-weight:800;
}
.pr-send:hover{background:var(--accent); color:var(--on-accent); filter:brightness(1.08);}

/* ==========================================================================
   YOU ARE HERE (js/78-you-are-here.js)

   One semester on a five-year map is the one you are in. Marking it is the
   difference between a plan you read and a plan you have to orient yourself
   in first. The accent rail runs the height of the semester so it is
   findable while scrolling past, not just at its heading. */
.imp-semester-block.sem-now{
  border-inline-start:3px solid var(--accent);
  padding-inline-start:13px;
  margin-inline-start:-16px;
  border-radius:0 10px 10px 0;
  background:linear-gradient(90deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%);
}
[dir="rtl"] .imp-semester-block.sem-now,
.rtl-mode .imp-semester-block.sem-now{
  background:linear-gradient(270deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%);
}
.now-tag{
  display:flex; align-items:center; gap:8px;
  margin:0 0 8px;
  font-size:10.5px; font-weight:800; letter-spacing:.08em; text-transform:uppercase;
  color:var(--accent);
}
.now-dot{
  flex:0 0 auto; width:8px; height:8px; border-radius:50%;
  background:var(--accent);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ==========================================================================
   FILTER, INSIDE THE METER (js/79-plan-filter.js)

   The chips ride in the hours meter because that is the one element on the
   plan that travels down the page, so the filter is still reachable four
   years in without a second sticky bar of its own. */
.pf-bar{
  display:flex; gap:6px; overflow-x:auto;
  margin-top:9px; padding-top:9px;
  border-top:1px solid var(--line);
  scrollbar-width:none;
}
.pf-bar::-webkit-scrollbar{display:none;}
.pf-chip{
  flex:0 0 auto; padding:6px 12px; border-radius:999px; cursor:pointer;
  font-family:inherit; font-size:11px; font-weight:800;
  background:var(--panel); color:var(--text-dim); border:1px solid var(--line);
  transition:background .14s ease, color .14s ease, border-color .14s ease;
}
.pf-chip:hover{border-color:var(--accent); color:var(--text);}
.pf-chip:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
/* Requirement chips carry the colour their cards carry, so the chip and what
   it reveals are recognisably the same thing. */
.pf-req{display:inline-flex;align-items:center;gap:6px;}
/* The chip says how many it would show. Dim, and tabular, so a row of chips
   reads as labels with counts rather than as two competing numbers. */
.pf-count{
  margin-inline-start:6px; font-size:10.5px; font-weight:700;
  color:var(--text-dim); font-variant-numeric:tabular-nums;
}
.pf-chip.pf-on .pf-count{color:inherit; opacity:.75;}
.pf-swatch{width:9px;height:9px;border-radius:2px;flex:none;}
.pf-div{
  flex:none;width:1px;align-self:stretch;margin:3px 2px;
  background:var(--line);
}
.pf-chip.pf-on{
  background:var(--accent); color:var(--on-accent); border-color:var(--accent);
}
/* Stuck to the top, the meter drops its caption (see .pw-stuck) but keeps
   the chips — being reachable while scrolling is the whole reason they are
   in here rather than in a row of their own. */
.sheet-plan.pw-stuck .pf-bar{margin-top:7px; padding-top:7px;}
@media (prefers-reduced-motion:reduce){ .pf-chip{transition:none;} }

/* ---- what each filter hides -------------------------------------------
   Three rules per mode, and no JavaScript walking the cards: hide what does
   not match, then :has() folds away any semester and any year left holding
   nothing. That last part is what makes a filtered plan show only the years
   that actually contain something.

   None of it touches the stored fold state, so whatever the student had
   open is still open when the filter comes off. */
.plan-filtering .imp-year-block.year-collapsed > .imp-year-body{display:block;}
.plan-filtering .connector-layer{display:none;}

.plan-f-avail  .course:not(.available),
.plan-f-locked .course.available,
.plan-f-locked .course.completed,
.plan-f-done   .course:not(.completed){display:none;}

.plan-f-avail  .imp-semester-block:not(:has(.course.available)),
.plan-f-done   .imp-semester-block:not(:has(.course.completed)),
.plan-f-avail  .imp-year-block:not(:has(.course.available)),
.plan-f-done   .imp-year-block:not(:has(.course.completed)){display:none;}
.plan-f-locked .imp-semester-block:not(:has(.course:not(.available):not(.completed))),
.plan-f-locked .imp-year-block:not(:has(.course:not(.available):not(.completed))){display:none;}

/* Filtering by requirement bucket (the chips that replaced My Path). The
   matching cards are marked .pf-hit in JS, so one rule covers all seven
   buckets instead of seven near-identical ones. */
.plan-f-req .course:not(.pf-hit){display:none;}
.plan-f-req .imp-semester-block:not(:has(.course.pf-hit)),
.plan-f-req .imp-year-block:not(:has(.course.pf-hit)){display:none;}

/* A pair bracket around a lecture whose lab the filter just hid would be a
   bracket around one card. */
.plan-filtering .cp-pair-label{display:none;}

/* A filter that matches nothing: say which nothing it is, and give the way
   back. Without this the plan simply goes blank, which reads as broken. */
.pf-empty{
  display:flex; flex-direction:column; align-items:center; gap:12px;
  margin:18px 0 4px; padding:26px 22px;
  border:1px dashed var(--line); border-radius:14px;
  background:var(--panel); text-align:center;
}
.pf-empty[hidden]{display:none;}
.pf-empty p{
  margin:0; max-width:34ch;
  font-size:13px; line-height:1.6; color:var(--text-dim);
}

