/* ===== PodsCool - dashboard ===== */
.dash {
  display: grid;
  grid-template-columns: 264px minmax(0, 1fr);
  min-height: 100vh;
  background:
    radial-gradient(1200px 600px at 100% -10%, rgba(179,162,232,.10), transparent 60%),
    radial-gradient(1000px 560px at -5% 110%, rgba(163,200,147,.12), transparent 55%),
    #f5f6f8;
  color: var(--ink);
}

/* THE PHONE TAB BAR IS PHONE-ONLY, AND MUST STAY OUT OF THE DESKTOP GRID.
   renderTabBar() in dashboard.html clears the `hidden` attribute unconditionally on boot, for
   every role and every width. Every rule that positions .dash__tabbar — the `position: fixed`
   that lifts it out of flow — lives inside @media (max-width: 900px). So on desktop the
   un-hidden <nav> had NO rule at all: it fell back to display:block and became a THIRD grid item
   of .dash (which is `264px minmax(0,1fr)`), wrapping the sidebar into the 1fr column and
   collapsing the real content into a 264px cell on row two. The dashboard then rendered blank
   with the phone tabs stacked in the top-left corner. A desktop default of display:none keeps it
   out of the grid; the @media (max-width:900px) block below turns it back on where it belongs. */
.dash__tabbar { display: none; }

/* ---------- Sidebar ---------- */
.side {
  position: sticky; top: 0; align-self: start; height: 100vh;
  display: flex; flex-direction: column;
  background: var(--glass);
  backdrop-filter: saturate(1.4) blur(8px);
  border-right: 1px solid var(--line);
  padding: 22px 16px;
  z-index: 20;
}
.side__brand { display: inline-flex; align-items: center; gap: 9px; font-family: var(--head); font-weight: 700; font-size: 20px; padding: 4px 8px 20px; }
.side__nav { display: flex; flex-direction: column; gap: 20px; flex: 1; overflow-y: auto; }
.side__group { display: flex; flex-direction: column; gap: 2px; }
.side__label { text-transform: uppercase; letter-spacing: .1em; font-size: 10.5px; font-weight: 700; color: var(--muted-2); padding: 0 10px 8px; }
.side__link {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; border-radius: 11px;
  font-size: 14.5px; font-weight: 500; color: var(--ink-2);
  background: none; border: 0; width: 100%; text-align: left; cursor: pointer;
  position: relative;
  transition: background .16s ease, color .16s ease, transform .16s ease;
}
.side__link svg { color: var(--muted-2); flex: 0 0 auto; transition: color .16s ease; }
.side__link:hover { background: var(--cream); color: var(--ink); }
.side__link:hover svg { color: var(--ink-2); }
.side__link.is-active {
  background: var(--ink-fill); color: #fff;
  box-shadow: 0 8px 20px rgba(15,23,42,.20);
}
.side__link.is-active svg { color: #fff; }
.side__link.is-active::before {
  content: ""; position: absolute; left: -16px; top: 50%; transform: translateY(-50%);
  width: 4px; height: 20px; border-radius: 0 4px 4px 0; background: var(--green);
}
.side__link .badge { margin-left: auto; background: var(--purple-tint); color: var(--purple-ink); font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 999px; }
/* Live unread-message count on the Messages item — a red circle, right-aligned. */
.side__link .side__badge { margin-left: auto; min-width: 19px; height: 19px; padding: 0 6px; background: #e5484d; color: #fff;
  font-size: 11px; font-weight: 700; line-height: 1; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; }
.side__link.is-active .side__badge { background: var(--bg); color: var(--muted); }
/* The `display:inline-flex` above out-specifies the UA's `[hidden]{display:none}`, so without
   this the badge NEVER hid — it just sat there as an empty red dot with no count. */
.side__link .side__badge[hidden] { display: none; }

.side__profile {
  display: flex; align-items: center; gap: 10px; margin-top: 14px;
  padding: 10px; border: 1px solid var(--line); border-radius: 14px; background: var(--bg);
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
.side__profile:hover { border-color: var(--green); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.side__avatar { width: 38px; height: 38px; border-radius: 50%; flex: 0 0 auto; object-fit: cover; background: var(--green-tint); display: grid; place-items: center; font-weight: 700; font-size: 14px; color: var(--green-ink); border: 1px solid var(--line-2); }
.side__pinfo { min-width: 0; flex: 1; }
.side__pinfo strong { display: block; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side__pinfo small { display: block; font-size: 12px; color: var(--muted); text-transform: capitalize; }
.side__profile .chev { color: var(--muted-2); }

/* ---------- Main ---------- */
.dash__main {
  padding: clamp(24px, 3vw, 40px) clamp(16px, 2vw, 32px) 64px;
  width: 100%;
  max-width: 1600px;
  margin: 0 auto;
  /* Grid items default to min-width:auto, which refuses to shrink below their content
     and lets wide rows spill outside the column. Needed at EVERY width, not just the
     small-screen breakpoint below. */
  min-width: 0;
}
.dash__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 28px; }
.dash__top h1 { font-family: var(--head); font-weight: 800; font-size: clamp(26px, 3vw, 34px); letter-spacing: -.02em; }
.dash__top p { color: var(--muted); font-size: 15px; margin-top: 4px; }
#top-actions { flex: 0 0 auto; }

/* ---------- Connect / setup banners ---------- */
.setup { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-bottom: 28px; }
.connect {
  display: flex; align-items: center; gap: 16px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 18px 20px; box-shadow: var(--shadow-xs); position: relative; overflow: hidden;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.connect:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--line-2); }
.connect::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; }
.connect--stripe::before, .connect--amber::before { background: var(--amber); }
.connect--zoom::before, .connect--purple::before { background: var(--purple); }
.connect--classroom::before, .connect--green::before { background: var(--green); }
.connect--photo::before { background: #7c3aed; }
.connect__ico { width: 46px; height: 46px; border-radius: 13px; display: grid; place-items: center; flex: 0 0 auto; }
.connect--stripe .connect__ico, .connect--amber .connect__ico { background: var(--cream); color: #b8860b; }
.connect--zoom .connect__ico, .connect--purple .connect__ico { background: var(--purple-tint); color: var(--purple-ink); }
.connect--classroom .connect__ico, .connect--green .connect__ico { background: var(--green-tint); color: var(--green-ink); }
.connect--photo .connect__ico { background: var(--cream); font-size: 24px; }
.connect__text { flex: 1; min-width: 0; }
.connect__text strong { display: block; font-size: 15px; font-weight: 700; }
.connect__text span { display: block; font-size: 13.5px; color: var(--muted); margin-top: 2px; }
.connect .btn { flex: 0 0 auto; }
.connect__done { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; color: var(--green-ink); font-weight: 700; font-size: 14px; }
.connect__action { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 12px; }
.connect__flash { display: block; font-size: 13px; font-weight: 600; margin-top: 6px; }

/* "Connecting to Stripe…" spinner (shown while we verify payout status). */
.connect__connecting { gap: 10px; color: var(--muted, var(--muted)); font-weight: 600; font-size: 14px; }
.connect__spinner {
  width: 22px; height: 22px;
  border: 2.5px solid var(--line, #e2e8f0);
  border-top-color: var(--green, #4f9a63);
  border-radius: 50%;
  display: inline-block;
  animation: cc-spin 0.7s linear infinite;
}
@keyframes cc-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .connect__spinner { animation-duration: 1.6s; } }
.connect__flash:empty { display: none; }

/* ---------- Stat cards ---------- */
.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); gap: 16px; margin-bottom: 24px; }
.scard {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 22px; box-shadow: var(--shadow-xs); position: relative; overflow: hidden;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.scard::after {
  content: ""; position: absolute; inset: 0 0 auto auto; width: 120px; height: 120px;
  background: radial-gradient(closest-side, rgba(163,200,147,.14), transparent);
  transform: translate(40%, -40%); pointer-events: none;
}
.scard:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--line-2); }
.scard--link { cursor: pointer; }
.scard--link:hover { border-color: var(--brand, #16a34a); }
.scard--link:focus-visible { outline: 2px solid var(--brand, #16a34a); outline-offset: 2px; }
.scard--link::before { content: "→"; position: absolute; top: 14px; right: 16px; font-size: 15px; color: var(--muted); opacity: 0; transform: translateX(-4px); transition: opacity .18s ease, transform .18s ease; }
.scard--link:hover::before, .scard--link:focus-visible::before { opacity: .8; transform: translateX(0); }
.panel--flash { animation: panelFlash 1.4s ease; }
@keyframes panelFlash { 0%, 100% { box-shadow: var(--shadow-xs); } 30% { box-shadow: 0 0 0 3px rgba(22,163,74,.35); } }
.scard__ico { width: 44px; height: 44px; border-radius: 13px; display: grid; place-items: center; margin-bottom: 16px; }
.scard__ico--pods { background: var(--green-tint); color: var(--green-ink); }
.scard__ico--students { background: var(--purple-tint); color: var(--purple-ink); }
.scard__ico--earn { background: var(--cream); color: var(--ink); }
.scard__ico--rating { background: var(--cream); color: #b8860b; }
.scard__num { font-family: var(--head); font-weight: 800; font-size: clamp(28px, 3vw, 32px); letter-spacing: -.02em; line-height: 1; }
.scard__label { color: var(--muted); font-size: 13.5px; margin-top: 6px; display: flex; align-items: center; gap: 6px; }

/* Info "i" chip + hover/focus tooltip on a stat tile. */
.scard__info {
  display: inline-grid; place-items: center;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--line, var(--cream)); color: var(--ink-2, var(--ink));
  font-family: var(--head); font-style: normal; font-weight: 700; font-size: 10px; line-height: 1;
  cursor: help; position: relative; flex: 0 0 auto;
}
.scard__info:hover, .scard__info:focus-visible { background: var(--purple-tint, var(--cream)); color: var(--purple-ink, var(--muted)); outline: none; }
.scard__tip {
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  width: 232px; background: var(--ink, #14151a); color: #fff;
  font-family: var(--sans); font-weight: 500; font-size: 12px; line-height: 1.45; text-transform: none;
  padding: 9px 11px; border-radius: 10px; box-shadow: var(--shadow-md);
  opacity: 0; visibility: hidden; transition: opacity .14s, transform .14s; pointer-events: none; z-index: 30;
}
.scard__tip::after {
  content: ""; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  border: 6px solid transparent; border-top-color: var(--ink, var(--ink));
}
.scard__info:hover .scard__tip, .scard__info:focus-visible .scard__tip { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(-2px); }

/* ---------- Panels ---------- */
.panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); gap: 16px; }
/* min-width:0 so long rows (e.g. an enrolled pod's title + teacher + price) truncate
   inside the card instead of pushing past its edge as the window narrows. */
.panel { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 24px; box-shadow: var(--shadow-xs); min-width: 0; }
.panel__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.panel__head h2 { font-family: var(--head); font-weight: 700; font-size: 17px; }
.panel__head a { font-size: 13.5px; color: var(--purple-ink); font-weight: 600; transition: opacity .15s; }
.panel__head a:hover { opacity: .75; }
.panel__empty { display: grid; place-items: center; text-align: center; min-height: 190px; padding: 24px; color: var(--muted); font-size: 14px; border: 1px dashed var(--line); border-radius: 12px; background: linear-gradient(180deg, #fafbfc, var(--bg)); }
.panel__empty .emoji { font-size: 28px; display: block; margin-bottom: 8px; filter: grayscale(.2); }

/* Overview panel rows: clickable session / activity items that route into the app.
   Capped height so a long list scrolls inside the panel instead of stretching it. */
.ov-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px;
  max-height: 288px; overflow-y: auto; overscroll-behavior: contain; padding-right: 4px; }
.ov-list::-webkit-scrollbar { width: 8px; }
.ov-list::-webkit-scrollbar-thumb { background: var(--line); border-radius: 8px; }
.ov-list::-webkit-scrollbar-thumb:hover { background: #c7ccd6; }
.ov-row { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg); color: inherit; text-decoration: none; transition: border-color .15s, background .15s, transform .05s; }
.ov-row:hover { border-color: var(--purple-ink); background: var(--bg); }
.ov-row:active { transform: translateY(1px); }
.ov-row__when { flex: none; min-width: 84px; display: flex; flex-direction: column; line-height: 1.2; }
.ov-row__when b { font-size: 13.5px; color: var(--ink); }
.ov-row__when span { font-size: 12px; color: var(--muted); }
.ov-row__title { flex: 1; font-size: 14px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-row__title b { font-weight: 700; }
.ov-row__go { flex: none; color: var(--purple-ink); font-weight: 700; }
.ov-row__ago { flex: none; font-size: 12px; color: var(--muted); }
.ov-row__dot { flex: none; width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center; background: var(--cream); color: var(--muted); font-size: 12px; }

.dash__mount { margin-top: 20px; }

/* ---------- Student review prompt (dashboard) ---------- */
.sh-review { display: flex; align-items: center; gap: 14px; background: linear-gradient(120deg, #fdf3dd, var(--bg) 70%); border: 1px solid #f0dca8; border-left: 5px solid var(--amber); border-radius: var(--radius-sm); padding: 16px 20px; margin-bottom: 20px; box-shadow: var(--shadow-xs); }
.sh-review__emoji { font-size: 30px; flex: 0 0 auto; }
.sh-review__txt { flex: 1; min-width: 0; }
.sh-review__txt strong { display: block; font-size: 16px; }
.sh-review__txt small { display: block; color: var(--ink-2); font-size: 13px; margin-top: 2px; }
.sh-review__more { font-size: 12px; font-weight: 700; color: #b8860b; background: var(--bg); border-radius: 999px; padding: 2px 8px; }
@media (max-width: 560px) { .sh-review { flex-wrap: wrap; } .sh-review #sh-rate { width: 100%; } }

/* ---------- Teacher review moderation (dashboard) ---------- */
.vreviews__summary { display: flex; align-items: center; gap: 18px; margin-bottom: 18px; }
.vreviews__big { font-family: var(--head); font-weight: 800; font-size: 40px; line-height: 1; color: var(--ink); }
.vreviews__big small { font-size: 18px; color: var(--muted-2); font-weight: 600; }
.vreviews__summary .star { font-size: 18px; }
.vreviews__summary p { color: var(--muted); font-size: 13.5px; margin-top: 6px; }
.vreviews { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
.vreview { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 16px 18px; box-shadow: var(--shadow-xs); }
.vreview__top { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.vreview__top .star { font-size: 16px; }
.vreview__who { font-size: 13px; font-weight: 700; color: var(--ink-2); }
.vreview__comment { color: var(--ink-2); font-size: 14px; line-height: 1.55; margin-bottom: 12px; }
.vreview__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }

/* ---------- Student home (custom overview) ---------- */
.sh-loading {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; min-height: 220px; padding: 48px 20px; text-align: center; color: var(--muted); font-size: 14px;
}
/* Brand loader (public/js/shared/pc-loader.js) is injected as <pc-podloader>; the old
   conic ring is gone. Element carries its own size; nudge it here if needed. */
.sh-loading pc-podloader { width: 76px; height: 76px; }

.sh-hero {
  background: linear-gradient(120deg, var(--green-tint), var(--bg) 70%);
  border: 1px solid var(--line); border-left: 5px solid var(--green);
  border-radius: var(--radius-sm); padding: 22px 24px; margin-bottom: 22px; box-shadow: var(--shadow-xs);
}
.sh-hero__label { font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--green-ink); margin-bottom: 12px; }
.sh-hero__row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.sh-hero__emoji { font-size: 40px; flex: 0 0 auto; }
.sh-hero__info { flex: 1 1 200px; min-width: 0; }
.sh-hero__info h2 { font-family: var(--head); font-size: 22px; letter-spacing: -.01em; }
.sh-hero__info p { color: var(--muted); font-size: 14px; margin-top: 2px; }
.sh-hero__join { flex: 0 0 auto; }
.sh-hero--empty { display: flex; align-items: center; gap: 16px; }
.sh-hero--empty h2 { font-family: var(--head); font-size: 19px; }
.sh-hero--empty p { color: var(--muted); font-size: 14px; margin-top: 2px; }

/* Full-width student overview — every section spans the whole main area. */
.student-home { max-width: none; }
.sh-section { margin-bottom: 34px; }
.sh-h { font-family: var(--head); font-size: 17px; margin-bottom: 20px; }

.sh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
.sh-card { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 16px 18px; box-shadow: var(--shadow-xs); transition: transform .16s, box-shadow .16s; }
.sh-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.sh-card__top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.sh-card__emoji { font-size: 26px; }
.sh-card h3 { font-family: var(--head); font-size: 16px; }
.sh-card__meta { color: var(--muted); font-size: 13px; margin-top: 2px; }
.sh-card__sched { color: var(--ink-2); font-size: 13px; margin: 8px 0 12px; }

.sh-pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 700; background: var(--cream); color: var(--ink-2); }
.sh-pill--ok { background: var(--green-tint); color: var(--green-ink); }

.sh-asgs { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.sh-asg { display: flex; align-items: center; gap: 11px; background: var(--bg); border: 1px solid var(--line); border-radius: 12px; padding: 11px 13px; box-shadow: var(--shadow-xs); text-decoration: none; color: inherit; transition: border-color .12s, box-shadow .12s, transform .12s; }
.sh-asg:hover { border-color: var(--purple); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.sh-asg__ic { flex: 0 0 auto; font-size: 18px; }
.sh-asg__body { flex: 1; min-width: 0; }
.sh-asg__body strong { display: block; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh-asg__body small { display: block; color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh-asg__pill { flex: 0 0 auto; font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 999px; background: var(--cream); color: var(--ink-2); white-space: nowrap; }
.sh-asg__pill--ok { background: var(--green-tint); color: var(--green-ink); }
.sh-asg__pill--miss { background: var(--cream); color: var(--muted); }
/* Graded is the end of the ladder, so it gets the brand purple rather than another green -
   at a glance "submitted" and "graded" must not look like the same state. */
.sh-asg__pill--grade { background: var(--purple-tint); color: var(--purple-ink); }
.sh-asgs__more { display: block; text-align: center; font-size: 13px; font-weight: 600; color: var(--purple-ink); text-decoration: none; padding: 6px; }
.sh-asgs__more:hover { text-decoration: underline; }
/* My pods — up to three vertical pod cards across; each holds its own assignments. */
/* Space above the first content section (My pods). The hero is ALSO a <section>, so
   :first-of-type doesn't hit My pods - target the section right after the grades strip
   (or after the hero when there are no grades yet). */
.sh-grades + .sh-section,
.sh-hero + .sh-section { margin-top: 34px; }
.sh-pods { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; align-items: start; }
.sh-pod { border: 1px solid var(--line); border-radius: 18px; overflow: hidden; box-shadow: var(--shadow-xs); }
.sh-pod__head { padding: 18px 18px 16px; cursor: pointer; transition: background .12s; }
.sh-pod__head:hover { background: var(--glass); }
.sh-pod__top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.sh-pod__emoji { font-size: 28px; }
.sh-pod__title { font-family: var(--head); font-size: 17px; line-height: 1.25; color: var(--ink); }
.sh-pod__meta { color: var(--muted); font-size: 13px; margin-top: 3px; }
.sh-pod__sched { color: var(--ink-2); font-size: 13px; margin: 8px 0 12px; }
/* The assignments tray under the header — white rows on a translucent panel. */
.sh-pod__work { background: var(--glass); border-top: 1px solid rgba(20,30,45,.06); padding: 13px 14px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.sh-pod__worktitle { font-size: 12px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin: 0 2px 2px; }
/* Only 3 rows show per pod; this carries you to the rest on the pod page. */
.sh-pod__more { display: block; text-align: center; font-size: 13px; font-weight: 600; color: var(--purple-ink); text-decoration: none; padding: 5px 6px 2px; }
.sh-pod__more:hover { text-decoration: underline; }
@media (max-width: 1100px) { .sh-pods { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px) { .sh-pods { grid-template-columns: 1fr; } }

/* This week — a full-width row of session cards (fills the screen, no cramped rail). */
.sh-week { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.sh-weekcard { display: flex; flex-direction: column; gap: 6px; background: var(--bg); border: 1px solid var(--line); border-left: 3px solid var(--purple); border-radius: 14px; padding: 16px 18px; box-shadow: var(--shadow-xs); transition: border-color .12s, box-shadow .12s, transform .12s; }
.sh-weekcard:hover { box-shadow: var(--shadow-sm); transform: translateY(-2px); }
.sh-weekcard__when { font-size: 12px; font-weight: 800; letter-spacing: .01em; color: var(--purple-ink); text-transform: uppercase; }
.sh-weekcard__title { font-family: var(--head); font-size: 16px; line-height: 1.25; color: var(--ink); }
.sh-weekcard__who { color: var(--muted); font-size: 12.5px; margin-bottom: 4px; }
.sh-weekcard .btn { margin-top: auto; }

.sh-parent { display: flex; align-items: center; gap: 12px; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 16px 18px; box-shadow: var(--shadow-xs); max-width: 460px; }
.sh-parent__ava { display: grid; place-items: center; width: 46px; height: 46px; border-radius: 50%; background: var(--purple-tint); color: var(--purple-ink); font-weight: 700; flex: 0 0 auto; }
.sh-parent strong { display: block; font-size: 15px; }
.sh-parent small { color: var(--muted); font-size: 12.5px; }

.sh-empty { display: grid; place-items: center; text-align: center; padding: 32px 20px; background: var(--bg); border: 1px dashed var(--line); border-radius: var(--radius-sm); }
.sh-empty--sm { padding: 22px 16px; }
.sh-empty__emoji { font-size: 30px; margin-bottom: 8px; }
.sh-empty p { color: var(--muted); font-size: 14px; max-width: 380px; margin-bottom: 14px; }

/* ---- Clickable student-home sections (open pod / go to view) ---- */
.is-clickable { cursor: pointer; }
.sh-hero.is-clickable:hover,
.sh-card.is-clickable:hover { border-color: var(--purple); box-shadow: var(--shadow-md); transform: translateY(-2px); }
.sh-week__item.is-clickable:hover { background: var(--bg); border-color: var(--purple); }
.is-clickable:focus-visible { outline: 2px solid var(--purple); outline-offset: 2px; }
/* Section headings that link to their full view */
.sh-h__link { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 5px; }
.sh-h__link::after { content: "›"; color: var(--muted-2); font-weight: 400; transition: transform .15s ease; }
.sh-h__link:hover { color: var(--purple-ink); }
.sh-h__link:hover::after { transform: translateX(3px); color: var(--purple-ink); }
/* Parent card + assignments empty become links */
.sh-parent--link, .sh-empty--link { text-decoration: none; color: inherit; transition: border-color .15s, box-shadow .15s, transform .15s; }
.sh-parent--link:hover { border-color: var(--purple); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.sh-empty--link:hover { border-style: solid; border-color: var(--purple); }
/* Parent/teacher panel headings that route to their view */
.panel__title-link { color: inherit; text-decoration: none; }
.panel__title-link:hover { color: var(--purple-ink); text-decoration: underline; }
.sh-empty p:last-child { margin-bottom: 0; }

.sh-gate { display: grid; place-items: center; text-align: center; padding: 56px 28px; background: linear-gradient(180deg, var(--purple-tint), var(--bg) 80%); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.sh-gate__emoji { font-size: 46px; margin-bottom: 14px; }
.sh-gate h2 { font-family: var(--head); font-size: 24px; margin-bottom: 10px; }
.sh-gate p { color: var(--muted); max-width: 460px; margin-bottom: 22px; line-height: 1.55; }

/* Google Classroom setup window (student home) */
.sh-setup { border: 1px solid var(--line); border-left: 4px solid #1a73e8; border-radius: var(--radius);
  background: linear-gradient(180deg, var(--wash), var(--bg) 70%); box-shadow: var(--shadow-sm);
  padding: 20px 22px; margin-bottom: 20px; }
.sh-setup__head { display: flex; gap: 14px; align-items: flex-start; }
.sh-setup__emoji { font-size: 30px; line-height: 1; }
.sh-setup__head h2 { font-family: var(--head); font-size: 19px; margin-bottom: 4px; }
.sh-setup__head p { color: var(--muted); font-size: 14px; line-height: 1.5; max-width: 640px; }
.sh-setup__err { margin: 12px 0 0; padding: 8px 12px; border-radius: 8px; background: var(--cream); color: var(--muted); font-size: 13px; }
.sh-setup__steps { list-style: none; margin: 16px 0 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.sh-setup__steps li { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--bg); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; }
.sh-setup__txt { flex: 1 1 260px; }
.sh-setup__txt strong { display: block; font-size: 14px; }
.sh-setup__txt small { color: var(--muted); font-size: 13px; }
.sh-setup__steps li .btn { margin-left: auto; }

@media (max-width: 900px) { .sh-cols { grid-template-columns: 1fr; } }
@media (max-width: 560px) {
  .sh-hero__info { flex-basis: 100%; }
  .sh-hero__join { width: 100%; }
  .sh-week__item { flex-wrap: wrap; }
  .sh-week__when { min-width: 0; flex-basis: 100%; }
}

/* ---------- Add-student modal (student-link.js) ---------- */
.as-lead { color: var(--muted); font-size: 14px; line-height: 1.55; margin-bottom: 16px; }
.as-lead strong { color: var(--ink); font-weight: 600; }
.as-hint { color: var(--muted); font-size: 13px; line-height: 1.5; margin: -6px 0 16px;
  padding: 10px 12px; background: var(--cream); border-radius: 10px; }
.as-hint strong { color: var(--ink); font-weight: 600; }
.as-hint a { color: var(--muted); text-decoration: none; font-weight: 600; }
.as-choice {
  display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 16px 18px; margin-bottom: 12px; cursor: pointer;
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
.as-choice:hover { border-color: var(--green); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.as-choice__ico { font-size: 26px; flex: 0 0 auto; }
.as-choice__txt { flex: 1; min-width: 0; }
.as-choice__txt strong { display: block; font-size: 15px; }
.as-choice__txt small { display: block; color: var(--muted); font-size: 13px; margin-top: 2px; }
.as-choice__chev { color: var(--muted-2); font-size: 22px; flex: 0 0 auto; }
.as-back { background: none; border: 0; color: var(--muted); font-size: 13.5px; font-weight: 600; cursor: pointer; padding: 0 0 12px; }
.as-back:hover { color: var(--ink); }
.btn-google.btn--block { width: 100%; justify-content: center; }

/* Student rows - badge for linked accounts vs managed profiles */
.stu-badge { display: inline-block; margin-left: 8px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--purple-ink); background: var(--purple-tint); padding: 1px 7px; border-radius: 999px; vertical-align: middle; }
.vcard__grow { flex: 1; min-width: 0; }
.link-muted { background: none; border: 0; color: var(--muted); font-size: 13px; font-weight: 600; cursor: pointer; }
.link-muted:hover { color: var(--ink); }

/* ---------- Tab views ---------- */
.view[hidden] { display: none; }
.vloading {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; min-height: 200px; padding: 48px 20px; text-align: center; color: var(--muted); font-size: 14px;
}
/* Brand loader (public/js/shared/pc-loader.js) is injected as <pc-podloader>; the old
   conic ring is gone. Element carries its own size. */
.vloading pc-podloader { width: 72px; height: 72px; }
.vsection { margin-bottom: 24px; }
.vsection__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
.vsection__head h2 { font-family: var(--head); font-weight: 700; font-size: 20px; letter-spacing: -.01em; }
.vsection__sub { color: var(--muted); font-size: 14px; margin-top: 3px; }
.vsection__cta { margin-top: 18px; }

.vempty { display: grid; place-items: center; text-align: center; padding: 56px 24px; background: var(--bg); border: 1px dashed var(--line); border-radius: var(--radius-sm); }
.vempty__emoji { font-size: 40px; margin-bottom: 12px; }
.vempty h3 { font-family: var(--head); font-size: 19px; margin-bottom: 6px; }
.vempty p { color: var(--muted); font-size: 14.5px; max-width: 440px; margin-bottom: 18px; }
.vempty p:last-child { margin-bottom: 0; }

.vpanel { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 24px; box-shadow: var(--shadow-xs); max-width: 620px; }
.vpanel--wide { max-width: none; } /* Find Teachers: fill the whole main area (dash__main caps at 1600) */

/* ---------- My Teachers (parent + student): card grid → full profile ---------- */
.mt-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); gap: 14px; }
.mt-card {
  display: flex; align-items: center; gap: 14px; text-align: left; width: 100%;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 16px 18px; box-shadow: var(--shadow-xs); cursor: pointer; font: inherit;
  transition: box-shadow .15s ease, border-color .15s ease, transform .15s ease;
}
.mt-card:hover { border-color: var(--green); box-shadow: 0 4px 16px rgba(20,30,45,.10); transform: translateY(-1px); }
.mt-card:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.mt-ava { width: 52px; height: 52px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; border: 2px solid #fff; box-shadow: 0 1px 6px rgba(20,30,45,.14); }
.mt-ava--init { display: inline-flex; align-items: center; justify-content: center; background: linear-gradient(135deg, var(--purple), var(--green)); color: #fff; font-weight: 700; font-size: 19px; }
.mt-card__body { display: flex; flex-direction: column; gap: 3px; flex: 1 1 auto; min-width: 0; }
.mt-card__name { font-family: var(--head); font-weight: 700; font-size: 16px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mt-card__meta { font-size: 13px; color: var(--muted); }
.mt-card__meta b { color: var(--ink); font-weight: 700; }
.mt-card__chev { flex: 0 0 auto; font-size: 24px; color: var(--muted); line-height: 1; }
.mt-back { margin-bottom: 16px; }
/* Verified tick on a Find Teachers result card. */
.rs-vb { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px;
  border-radius: 50%; background: var(--green, #4f9a63); color: #fff; font-size: 10px; font-weight: 800;
  vertical-align: 2px; }
.vnote { background: var(--green-tint); color: var(--green-ink); border-radius: var(--radius-sm); padding: 16px 18px; font-size: 14px; line-height: 1.5; margin-top: 14px; }
.vnote a { color: var(--green-ink); font-weight: 700; text-decoration: underline; }

.vgroup { margin-bottom: 26px; }
.vgroup__title { display: flex; align-items: baseline; gap: 10px; font-size: 15px; font-weight: 700; margin: 0 0 12px; }
.vgroup__count { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.vcards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr)); gap: 14px; }
.vcard { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 18px; box-shadow: var(--shadow-xs); }
.vcard h3 { font-family: var(--head); font-size: 16px; margin-bottom: 4px; }
.vcard__meta { color: var(--muted); font-size: 13.5px; margin-bottom: 12px; }
.vcard--row { display: flex; align-items: center; gap: 12px; }
.vcard--link { display: block; text-decoration: none; color: inherit; transition: border-color .12s, box-shadow .12s, transform .12s; }
.vcard--link:hover { border-color: var(--green, #4f9a63); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.vcard__go { display: inline-block; margin-top: 10px; color: var(--green-ink, var(--muted)); font-weight: 700; font-size: 13.5px; }
.vcard--row strong { display: block; font-size: 15px; }
.vcard--row small { color: var(--muted); font-size: 13px; }

.vava { display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: var(--green-tint); color: var(--green-ink); font-weight: 700; font-size: 12px; margin-right: 8px; flex: 0 0 auto; }
.vava--lg { width: 44px; height: 44px; font-size: 15px; margin: 0; }
.vava--xl { width: 60px; height: 60px; font-size: 20px; margin: 0; }
img.vava { object-fit: cover; }
img.side__avatar { object-fit: cover; }
.vprofile__chips { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 8px; line-height: 1; }
.vprofile__chips > span:empty { display: none; }
/* Collapse the #set-badge wrapper so the badge itself is the flex child - no wrapper
   line-box can shift it off the Teacher pill's centerline. */
.vprofile__chips #set-badge { display: contents; }
/* Teacher pill and Verified badge must be the exact same size and sit on the same line. */
.vprofile__chips .vpill,
.vprofile__chips .vbadge { padding: 3px 10px; font-size: 12px; font-weight: 700; line-height: 1.4; border-radius: 999px; }
.vprofile__chips .vbadge { display: inline-flex; align-items: center; gap: 5px; }
.vprofile__chips .vbadge svg { width: 13px; height: 13px; }
.vphoto-btn { display: inline-flex; align-items: center; margin: 0; background: none; border: 0; padding: 0 2px; color: var(--muted); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
.vphoto-btn:hover { color: var(--muted); }
.vphoto-btn:disabled { opacity: .6; cursor: default; }
/* A SPINNING RING, NOT AN ELLIPSIS, WHILE THE PHOTO FLOW IS BUSY. Gerald, testing on
   production: "The three dots trips me out. Maybe a 'circular icon' for loading." Three dots
   read as a truncated label rather than as progress - he clicked them expecting a menu. Reuses
   the cc-spin keyframes and the reduced-motion rule defined above for .connect__spinner, so
   there is one spinner vocabulary in this file rather than two. currentColor keeps it on the
   button's own tint in either theme. */
.vspin {
  width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  display: inline-block; vertical-align: -1px;
  animation: cc-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .vspin { animation-duration: 1.6s; } }

.vtable-wrap { overflow-x: auto; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); box-shadow: var(--shadow-xs); }
.vtable { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 480px; }
.vtable th { text-align: left; padding: 13px 18px; font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted-2); border-bottom: 1px solid var(--line); background: var(--wash); }
.vtable td { padding: 13px 18px; border-bottom: 1px solid var(--line-2); vertical-align: middle; }
.vtable tr:last-child td { border-bottom: 0; }
.vtable td small { color: var(--muted); }

.vpill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; background: var(--cream); color: var(--ink-2); }
.vpill--ok { background: var(--green-tint); color: var(--green-ink); }
.vpill--warn { background: var(--cream); color: #b8860b; }
.vpill--soft { background: var(--purple-tint); color: var(--purple-ink); text-transform: capitalize; }

.vsched { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
.vsched__day h3 { font-family: var(--head); font-size: 15px; margin-bottom: 10px; color: var(--ink); }
.vsched__item { display: flex; align-items: center; gap: 16px; background: var(--bg); border: 1px solid var(--line); border-left: 3px solid var(--green); border-radius: 12px; padding: 12px 16px; margin-bottom: 8px; box-shadow: var(--shadow-xs); }
.vsched__time { flex: 0 0 90px; font-weight: 700; font-size: 13.5px; color: var(--green-ink); }
.vsched__body strong { display: block; font-size: 14.5px; }
.vsched__body small { color: var(--muted); font-size: 12.5px; text-transform: capitalize; }

.vprofile { display: flex; align-items: center; gap: 16px; padding-bottom: 20px; margin-bottom: 20px; border-bottom: 1px solid var(--line); }
.vprofile strong { display: block; font-size: 17px; font-family: var(--head); }
.vprofile small { display: block; color: var(--muted); font-size: 13.5px; margin: 2px 0 8px; }
.vfield { display: block; margin-bottom: 16px; }
.vfield > span { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink-2); margin-bottom: 7px; }
.vfield input { width: 100%; font: 400 15px var(--sans); color: var(--ink); border: 1px solid var(--line); border-radius: var(--radius-btn); padding: 12px 14px; background: var(--bg); }
.vfield input:disabled { background: var(--wash); color: var(--muted); }
.vfield input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px rgba(163,200,147,.25); }
.vfield-status { font-size: 13.5px; font-weight: 600; min-height: 18px; margin-bottom: 8px; }
.vfield-status.ok { color: var(--green-ink); }
.vfield-status.err { color: var(--muted); }
.vactions { display: flex; gap: 10px; flex-wrap: wrap; }

.vfaq { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; max-width: 720px; }
.vfaq__item { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 4px 18px; box-shadow: var(--shadow-xs); }
.vfaq__item summary { cursor: pointer; padding: 14px 0; font-weight: 600; font-size: 15px; list-style: none; display: flex; justify-content: space-between; align-items: center; }
.vfaq__item summary::-webkit-details-marker { display: none; }
.vfaq__item summary::after { content: "＋"; color: var(--muted-2); font-weight: 400; }
.vfaq__item[open] summary::after { content: "－"; }
.vfaq__item p { padding: 0 0 16px; color: var(--muted); font-size: 14px; line-height: 1.55; max-width: 640px; }

/* keep classroom.js panel readable inside the dashboard */
.dash__mount .dash-card, .dash__mount .dash-card--classroom { margin: 0; }

/* ---------- Loading / responsive ---------- */
.dash__loading { padding: 60px; text-align: center; color: var(--muted); grid-column: 1 / -1; }

/* ---------- Mobile: the sidebar becomes a drawer ---------- */
/* It used to reflow into a wrapped row of every nav item, group headers hidden — which turned
   a 12-item teacher nav into an unreadable block of pills. Below 900px the whole sidebar slides
   in from the left instead, keeping its real structure (groups, labels, profile, legal) intact. */
.dash__mbar { display: none; }
.dash__tabs { display: none; }
/* THE WAY OUT, ON DESKTOP TOO. Gerald's 133: "Add a homepage button to leave." He was not
   failing to find it - above 900px it did not exist. This rule was display:none and the 900px
   block below turned it back on, so Home / Browse Pods / How It Works were a MOBILE-DRAWER-ONLY
   affordance and a desktop dashboard's only exit was the brand logo in the corner.

   It lands between .side__nav (flex:1, so the nav takes the slack) and .side__profile, which
   pins it to the BOTTOM of the sidebar - where a control for leaving belongs, rather than in
   among the dashboard's own sections where it would read as a twelfth destination. The hairline
   is what separates those two jobs.

   Verified by enumeration rather than by eye: /dashboard redirects to /login when signed out,
   so it cannot be rendered here. Exactly two rules in the whole codebase touch .side__site -
   this one and the 900px override below, same selector, no specificity conflict and nothing
   inline in the markup. */
.side__site { display: flex; flex-direction: column; gap: 2px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line-2); }
.dash__burger { display: none; margin-left: auto; background: none; border: 0; padding: 10px; cursor: pointer;
  border-radius: 10px; }
.dash__burger span { display: block; width: 22px; height: 2px; background: var(--ink-fill); border-radius: 2px;
  transition: transform .18s ease, opacity .18s ease; }
.dash__burger span + span { margin-top: 5px; }
/* THE BARS GO WHITE IN DARK MODE, and they were #0f172a on a #14151a bar: measured 1.02:1, which
   is not low contrast — it is the same colour twice. Victor, 2026-09-18: "the menu bar ... it's
   currently blue on a dark background."

   var(--ink-fill) is a FILL token and is deliberately not remapped for dark; inverting it would
   flip every dark button on the site. styles.css:4903 already records this exact fault and this
   exact remedy for the marketing header's .nav__toggle — the dashboard's burger is the same bug in
   the other file, and was missed because it only exists below 900px, where no desktop check looks.
   White measures 18.23:1 on the same bar. */
.pc-dark .dash__burger span { background: #ffffff; }
.dash__burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.dash__burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.dash__burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.side-scrim { display: none; }

@media (max-width: 900px) {
  /* Block, not a 1-column grid: the drawer and scrim are position:fixed and drop out of flow,
     so the grid's min-height:100vh would split the remaining rows evenly and stretch the top
     bar to half the screen. */
  .dash { display: block; }
  .side, .dash__main { min-width: 0; }

  .dash__mbar { display: flex; align-items: center; gap: 10px; padding: 10px 16px;
    background: var(--bg); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 60;
    flex-wrap: wrap; /* row 2 is the tab strip */ }
  .dash__burger { display: block; }

  /* THE TWO CHROME CONTROLS SIT TOGETHER at the right of the bar: [theme][menu]. The burger owns
     `margin-left: auto` in its base rule, which is what pushes it away from the brand; with the
     toggle moved in beside it BOTH would have had an auto margin and flexbox would have split the
     free space between them, stranding the toggle mid-bar. So the auto moves to whichever comes
     first, and the burger gives its up when the toggle is its immediate predecessor. */
  .dash__mbar .theme-toggle { margin-left: auto; flex: 0 0 auto; }
  .dash__mbar .theme-toggle + .dash__burger { margin-left: 0; }

  /* THE DRAWER CARRIES THE WHOLE ROLE NAV AGAIN, and this rule used to be `display: none`.
     That was right while the scrolling pill strip carried every section: two menus listing the
     same sixteen things would have been worse than one. The strip is gone (see .dash__tabsrow
     below) and the phone tab bar holds FIVE destinations, so the other eleven have to live
     somewhere reachable — Victor, 2026-09-18: "I need you to use the menu ba to navigate pages."
     This is that menu. It is also what the iOS app does with a student's More tab. */
  #side-nav { display: block; }
  /* The drawer shows this as its PRIMARY nav rather than as a footer, so it drops the
     separator the desktop rule adds - a hairline above the first item of a list it leads. */
  .side__site { display: flex; flex-direction: column; gap: 2px; margin: 4px 0 auto;
    padding-top: 0; border-top: 0; }

  /* The signed-in role's tabs, out in the open. One scrollable row as the bar's second line —
     inside .dash__mbar so it stays stuck to the top along with the brand.

     The row is a flex PAIR: a fixed back circle (level 2 only) and the scrolling pills beside
     it. The circle is a sibling of the scroller rather than its first child, which is the whole
     point — a child would scroll away with the pills. */
  /* THE SCROLLING PILL STRIP IS OFF. It rendered every nav item the role has, which for a
     teacher is 16 pills — measured on a 359px screen: 1524px of strip, so 1165px of it, 76% of
     the navigation, sat off the right edge behind a horizontal swipe with nothing on screen
     saying it was there. Replaced by .dash__tabbar (five fixed destinations, the app's five)
     plus the drawer above (everything). The rules are kept rather than deleted because the
     markup and renderTabStrip() are still there for a tablet arrangement later. */
  .dash__tabsrow { display: none; }

  .dash__tabsback { flex: 0 0 auto; width: 34px; height: 34px; padding: 0; border: 0;
    border-radius: 50%; background: var(--ink, #14202b); color: #fff; cursor: pointer;
    display: grid; place-items: center; -webkit-appearance: none; appearance: none;
    box-shadow: 0 2px 8px rgba(20, 30, 45, .2); }
  .dash__tabsback[hidden] { display: none; }
  .dash__tabsback:active { transform: scale(.94); }

  /* The negative margins bleed the scroll area to the screen edge so the row scrolls
     edge-to-edge; the padding keeps the first pill clear of the bezel. */
  .dash__tabs { display: flex; flex: 1 1 auto; gap: 6px; overflow-x: auto; min-width: 0;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    margin: 0 -16px 0 0; padding: 2px 16px 2px 0; }
  .dash__tabs::-webkit-scrollbar { display: none; }
  .dash__tabs[hidden] { display: none; }
  .dash__tab { flex: 0 0 auto; border: 1px solid var(--line); background: var(--bg); cursor: pointer;
    border-radius: 999px; padding: 7px 14px; font: inherit; font-size: 13px; font-weight: 600;
    color: var(--muted); white-space: nowrap; transition: background .15s, color .15s, border-color .15s;
    text-decoration: none; }
  .dash__tab.is-active { background: var(--ink, #14202b); border-color: var(--ink, var(--ink)); color: #fff; }

  .side {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 80;
    width: min(84vw, 320px); height: 100dvh; overflow-y: auto;
    background: var(--bg); border-right: 1px solid var(--line);
    box-shadow: 0 10px 40px rgba(20, 30, 45, .18);
    transform: translateX(-100%); transition: transform .22s ease;
  }
  .side.is-open { transform: none; }
  /* The drawer carries its own brand at the top; the bar's copy is the one you see when closed. */
  .side__brand { padding: 4px 0 2px; }

  /* pointer-events: the scrim is ALWAYS display:block here (author display beats the [hidden]
     attribute), which meant an invisible full-screen layer sat at z-70 over the bar (z-60) and
     every view — the entire mobile dashboard was tap-dead. It must only catch taps while the
     drawer is actually open; when closed, touches pass straight through to the page. */
  .side-scrim { display: block; position: fixed; inset: 0; z-index: 70; pointer-events: none;
    background: rgba(16, 22, 30, .42); opacity: 0; transition: opacity .22s ease; }
  .side-scrim.is-open { opacity: 1; pointer-events: auto; }

  /* ---------------------------------------------------------------------------------------
     THE PHONE TAB BAR — the iOS app's tab bar, on the web.

     FIVE EQUAL COLUMNS, NOT FLEX: grid-template-columns: repeat(5, 1fr) makes every tab the
     same width whatever its caption, so "Assignments" cannot squeeze "More" to nothing. It is
     also what stops the bar reflowing as labels change between roles.

     SAFE AREA IS NOT OPTIONAL. On a notched iPhone in Safari the home indicator sits over the
     bottom ~34px; without env(safe-area-inset-bottom) the row of tabs is half under it and the
     centre tab — the one the app deliberately puts under the thumb — is the worst hit.

     Z-INDEX 65 SITS BETWEEN THE PAGE AND THE SCRIM (70). Above the content so it always floats;
     BELOW the scrim so opening the drawer dims the bar too, instead of leaving five bright tabs
     over a darkened page looking like they are still live. */
  .dash__tabbar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 65;
    display: grid; grid-template-columns: repeat(5, 1fr);
    background: var(--bg); border-top: 1px solid var(--line);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-shadow: 0 -2px 14px rgba(20, 30, 45, .07);
  }
  .dash__tabbar[hidden] { display: none; }

  /* 52px of tappable height before the safe area — above Apple's 44pt minimum, which the old
     strip's pills (34-40px) were under. */
  .dash__tb {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
    min-height: 52px; padding: 7px 2px 8px; border: 0; background: none; cursor: pointer;
    font: inherit; color: var(--muted); text-decoration: none; -webkit-tap-highlight-color: transparent;
    -webkit-appearance: none; appearance: none;
  }
  .dash__tbico { display: grid; place-items: center; height: 23px; }
  .dash__tblab { font-size: 10.5px; font-weight: 600; letter-spacing: .01em; line-height: 1;
                 max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dash__tb.is-on { color: var(--green-ink, #5f8149); }
  .dash__tb:active { transform: scale(.94); }
  @media (prefers-reduced-motion: reduce) { .dash__tb:active { transform: none; } }

  /* THE BELL AND THE REPORT FLAG SAT ON TOP OF TWO TABS. Both are position:fixed, bottom:20px,
     z-index:900 — so the moment a 53px bar appeared under them they covered it, and at z-900 they
     won. Measured with elementFromPoint at each tab's centre: "Messages" returned pcn-bell and
     "Account" returned report-fab. Two of the five tabs could not be tapped at all, and nothing
     about the screen said so — the tabs are drawn, they just are not what your thumb reaches.

     Lifted rather than re-stacked: putting the bar above them would hide a live notification
     count behind it. `.pc-tabbar` is set by renderTabBar so this cannot reach pod-class.html or
     preview.html, which load this stylesheet and have no bar. */
  .pc-tabbar .report-fab,
  .pc-tabbar .pcn-bell { bottom: calc(53px + 14px + env(safe-area-inset-bottom, 0px)); }

  /* THE BAR IS FIXED, SO IT COVERS THE LAST 60-ODD PIXELS OF EVERY PAGE unless the page is told
     to end above it. Without this the final row of any list — the last pod card, the Save button
     at the foot of a form — sits under the bar and cannot be tapped. */
  .dash__main { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
}
@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .side, .side-scrim { transition: none; }
}
@media (max-width: 560px) {
  /* Overview stat cards: a 2x2 grid, not four full-width slabs. The auto-fit track floors at
     200px, so on a ~358px content width only ONE column ever fit and each card became a
     screen-filling block — four scrolls to read four numbers. Two fixed columns instead, with
     the padding, icon and type stepped down so a pair sits comfortably side by side. */
  .stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 18px; }
  .scard { padding: 14px 14px 16px; }
  .scard__ico { width: 34px; height: 34px; border-radius: 10px; margin-bottom: 10px; }
  .scard__num { font-size: 24px; }
  .scard__label { font-size: 12.5px; margin-top: 4px; line-height: 1.3; }
  /* The hover arrow is meaningless on touch and steals a corner of a smaller card. */
  .scard--link::before { display: none; }

  .dash__top { flex-direction: column; }
  #top-actions, #top-actions .btn { width: 100%; }
  .connect { flex-wrap: wrap; }
  .connect .btn, .connect__done { width: 100%; justify-content: center; }
}

/* ---------- Students (parent) ---------- */
.stu-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.stu-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg); transition: border-color .15s; }
.stu-row:hover { border-color: var(--line-2); }
.stu-av { width: 38px; height: 38px; border-radius: 50%; flex: 0 0 auto; display: grid; place-items: center; font-weight: 700; font-size: 15px; background: var(--green-tint); color: var(--green-ink); }
.stu-info { flex: 1; min-width: 0; }
.stu-info strong { display: block; font-size: 15px; }
.stu-info small { color: var(--muted); font-size: 12.5px; }
.stu-del { border: 0; background: none; color: var(--muted-2); cursor: pointer; width: 30px; height: 30px; border-radius: 8px; font-size: 13px; flex: 0 0 auto; }
.stu-del:hover { background: var(--cream); color: var(--muted); }
.sm-note { font-size: 13px; color: var(--muted); margin-bottom: 16px; line-height: 1.55; }

/* ---------- Enrollments (parent) ---------- */
.enr-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.enr-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg); }
.enr-row--link { cursor: pointer; transition: border-color .12s, box-shadow .12s, transform .12s; }
.enr-row--link:hover { border-color: var(--brand, #16a34a); box-shadow: 0 2px 10px rgba(16,163,74,.12); transform: translateY(-1px); }
.enr-row--link:focus-visible { outline: 2px solid var(--brand, #16a34a); outline-offset: 2px; }
.enr-ic { width: 38px; height: 38px; border-radius: 11px; flex: 0 0 auto; display: grid; place-items: center; font-size: 18px; background: var(--purple-tint); }
.enr-info { flex: 1; min-width: 0; }
.enr-info strong { display: block; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.enr-info small { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.enr-pill { flex: 0 0 auto; font-size: 11.5px; font-weight: 700; padding: 3px 10px; border-radius: 999px; background: var(--cream); color: #b8860b; white-space: nowrap; }
.enr-pill--ok { background: var(--green-tint); color: var(--green-ink); }

/* ===== [session-3] cancel-modal loading spinner ===== */
.cxl-loading { display: flex; flex-direction: column; align-items: center; gap: 12px; color: var(--muted, #8a94a0); text-align: center; padding: 26px 20px; }
.cxl-loading p { margin: 0; font-size: 14px; }
.cxl-spin { display: inline-block; width: 26px; height: 26px; border: 3px solid rgba(109,75,216,.22); border-top-color: var(--muted); border-radius: 50%; animation: cc-spin .7s linear infinite; }
.cxl-spin--btn { width: 15px; height: 15px; border-width: 2px; border-color: rgba(255,255,255,.5); border-top-color: #fff; margin-right: 8px; vertical-align: -2px; }

/* ===== [session-3] parent "Rate your teachers" panel ===== */
.prv-panel { background: var(--bg); border: 1px solid var(--line, #eef0f2); border-radius: 14px; padding: 16px 18px; margin-bottom: 20px; box-shadow: var(--shadow-xs, 0 1px 2px rgba(20,30,45,.04)); }
.prv-h { font-size: 15px; margin: 0 0 2px; color: var(--ink); }
.prv-sub { font-size: 12.5px; color: var(--muted, #8a94a0); margin: 0 0 12px; }
.prv-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid #f3f4f6; }
.prv-row:first-of-type { border-top: 0; }
.prv-info { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.prv-info strong { font-size: 14px; color: var(--ink); }
.prv-info small { font-size: 12px; color: var(--muted, #8a94a0); }
.prv-stars { color: #e3b23c; letter-spacing: 1px; font-size: 14px; }
.prv-btn { flex: 0 0 auto; }

/* [work/admin-console-roundout] Post-checkout toast - view-independent, auto-dismisses. */
.co-toast { position: fixed; top: 18px; left: 50%; transform: translateX(-50%); z-index: 4000;
  max-width: 440px; width: calc(100% - 32px); display: flex; flex-direction: column; gap: 2px;
  padding: 13px 18px; border-radius: 12px; box-shadow: 0 12px 34px rgba(20,21,26,.18);
  transition: opacity .35s ease; font-family: var(--sans); }
.co-toast strong { font-size: 14.5px; }
.co-toast span { font-size: 13px; opacity: .9; line-height: 1.45; }
.co-toast--ok { background: var(--cream); color: var(--muted); border: 1px solid #bfe0cc; }
.co-toast--warn { background: var(--cream); color: var(--muted); border: 1px solid #f0dca8; }

/* Pod Builder: the document list, and the editor behind it. */
.cb-listbar { display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; margin: 4px 0 14px; }
.cb-listbar .btn { flex: none; }
.cb-listbar .btn[disabled] { opacity: .5; cursor: not-allowed; }

/* ── THE POD BUILDER WORKSPACE ────────────────────────────────────────────
   Controls left, document right, one page. A teacher setting a course up is holding three things
   at once — what the pod IS, what the course should CONTAIN, and what came back — and the old page
   made each of them replace the last. The editor opens into the right pane instead of taking over.
   Left rail sticks; only the document scrolls, which is the half that gets long. */
/* THE RAIL MUST NOT SCROLL. Every control here is set once before pressing Draft, and a teacher
   who has to scroll a settings column to find the button that uses those settings has to hold the
   top of it in their head. It was overflowing by roughly a third — so the density changed, not the
   height: tighter gaps, two-column facts, shorter text areas, and the button pinned to the bottom
   of the card rather than living at the end of a scroll. */
/* THE MARGINS GIVE THE ROOM, NOT THE TYPE. The first pass at fitting this rail shrank every
   label and figure to 11-12px, which is the wrong lever — the space was going to page padding,
   not to content. .dash__main pads 24-40px top and 16-32px each side and .vsection__head adds
   another 18px below the title, all of it around a workspace that wants the width. Clawed back
   here for this view only; every other view keeps the document-style gutters it was designed for. */
/* NOTHING HERE SCROLLS EXCEPT THE DOCUMENT. The rail was position:sticky, which is bounded by its
   containing block — and when the session list is short the grid row is only as tall as the rail
   itself, leaving no slack to stick in. So the rail scrolled away with the page exactly when there
   was least reason to, and sticky gave no sign it had failed: no error, no warning, correct CSS,
   wrong behaviour, entirely dependent on how long the OTHER column happened to be.
   
   A fixed-height workspace removes the dependency. The grid is exactly the viewport minus the page
   chrome, the rail is simply always there, and the document pane does the scrolling — which is the
   half that gets long and the only half that should move. */
/* THE RAIL SHRINKS BEFORE THE LAYOUT BREAKS. A fixed 336 with a breakpoint under it means the
   two columns survive exactly until they don't, and where that cliff sits is a guess about
   somebody's window — one I got wrong twice, first at 1080 and then at 820, while the teacher
   watching a draft kept finding the documents a full screen below the form.
   
   clamp() removes the guess. The rail gives up width gradually as the window narrows and stops
   at 268, which is what the pod-facts grid needs before its two columns collide. Stacking is
   then a genuine last resort rather than the first thing that happens. */
.cb-work { display: grid; grid-template-columns: clamp(308px, 32vw, 386px) minmax(0, 1fr); gap: 18px;
  /* NO NEGATIVE MARGIN. This used to pull the workspace back out to the window edge with
     calc(-1 * clamp(10px, 1.8vw, 30px)), which cancels almost all of .dash__main's
     clamp(16px, 2vw, 32px) — about 2px survived on a wide screen. The rail and the session
     rows then sat flush against the glass while the "Pod Builder" heading above them kept
     the page padding, so the content read as wider than its own title and the whole view
     looked clustered against the edges. Width for this view comes from the 1840px max-width
     below, which widens the column without eating the margin. */
  min-width: 0;
  /* 75 = where the workspace actually starts (63) plus the page's remaining tail (12). Measured on
     the rendered page, not guessed: the rail's content is 814px, so it fits with 11px to spare and
     the page itself has nothing left to scroll. */
  height: calc(100vh - var(--cb-chrome, 75px)); align-items: stretch; }
/* THE CARD IS AS TALL AS WHAT IS IN IT. Stretching it to the grid's full height left a slab of
   empty white below the button — the card looked broken rather than roomy, and the eye reads the
   bottom of a card as the end of its content. The grid keeps its height so the DOCUMENT side still
   fills the screen and scrolls; only the rail stops at its own end. */
.cb-rail { align-self: start; max-height: 100%; }
.cb-rail { display: flex; flex-direction: column; gap: 10px; min-height: 0;
  background: var(--bg); border-radius: 16px; padding: 15px 15px 12px;
  box-shadow: 0 1px 2px rgba(20,21,26,.06), 0 10px 30px rgba(20,21,26,.06);
  /* Only if a very short window forces it. At every normal height it never engages. */
  overflow-y: auto; }
/* The document side owns the scroll. */
/* padding-bottom because this is the SCROLL CONTAINER. The last session row sat flush against the
   bottom edge of the window — nothing was cut off, but a list that ends exactly at the viewport
   reads as one that has more below it, so people keep trying to scroll. */
.cb-stage { min-height: 0; overflow-y: auto; padding-right: 2px; padding-bottom: 72px; }
.cb-rail__block { display: flex; flex-direction: column; gap: 9px; }
.cb-rail__lab { font-size: 13.5px; font-weight: 700; color: var(--muted); }
.cb-rail__h { font-family: var(--head); font-size: 16px; margin: 4px 0 4px; }
.cb-rail select, .cb-rail textarea, .cb-rail input[type="number"] {
  font: inherit; font-size: 14px; border: 1px solid var(--line-2, #efeeea); border-radius: 10px;
  padding: 9px 11px; background: var(--bg); width: 100%; }
.cb-rail textarea { min-height: 0; height: 34px; transition: height .16s ease; overflow-y: hidden; }
.cb-rail textarea:focus, .cb-rail textarea:not(:placeholder-shown) { height: 62px; overflow-y: auto; }
.cb-rail textarea { resize: vertical; }
.cb-rail__foot { padding-top: 9px; margin-top: 0;
  border-top: 1px solid var(--line-2, #efeeea); display: flex; flex-direction: column; gap: 5px; }
.cb-rail__foot .btn { width: 100%; }

/* WHAT THE DRAFT WILL BE BUILT FROM. All of it already reached the model; none of it was ever
   shown, so the only way to learn what a draft was based on was to read the draft. */
.cb-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; margin-top: 1px; }
.cb-fact { display: flex; flex-direction: column; gap: 1px; font-size: 13px; line-height: 1.4;
  padding: 7px 10px; background: var(--wash); border-radius: 8px; min-width: 0; }
/* Runs and Cadence carry dates and day lists, which do not survive half a column. */
.cb-fact:nth-child(5), .cb-fact:nth-child(6) { grid-column: 1 / -1; flex-direction: row;
  justify-content: space-between; align-items: baseline; }
.cb-fact dt { color: var(--muted); margin: 0; }
.cb-fact dd { margin: 0; font-weight: 700; color: var(--ink, var(--ink));
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cb-opt { display: flex; flex-direction: column; gap: 6px; font-size: 13.5px; font-weight: 600;
  color: var(--muted); }
.cb-opt span { display: flex; justify-content: space-between; align-items: baseline; }
.cb-opt span b { font-size: 14px; color: var(--green-ink, var(--muted)); }
.cb-opt input[type="range"] { width: 100%; accent-color: var(--muted); }
.cb-opt__hint { margin: -3px 0 0; font-size: 12.5px; font-weight: 400; color: var(--muted-2); line-height: 1.45; }
/* TWO BANDS DOWN THE RAIL, so eight controls and five hints stop reading as one column.
   The panel is a white card and every field is white with a hairline, so the borders were carrying
   all of the separation and none of the grouping — eleven rows at identical weight and identical
   gap, with nowhere for the eye to rest. The tint is the one .cb-fact already uses rather than a
   new colour, and the fields keep their white so they lift off it instead of dissolving into it.
   No copy changed and no control moved: the three dropdowns are how the course is BUILT, the rest
   is what goes IN it, and that division already existed in the order they were written. */
.cb-band { display: flex; flex-direction: column; gap: 10px;
  padding: 12px 12px 13px; border-radius: 12px; background: var(--wash); }
/* Tighter INSIDE a band than between them, or the grouping is undone by the rhythm. */
.cb-band .cb-opt { font-size: 13px; gap: 5px; }
.cb-band .cb-opt__hint { margin: -3px 0 1px; font-size: 12px; line-height: 1.4; }
.cb-band .cb-sched { gap: 5px; }
/* The last hint in a band has nothing under it, so its bottom margin is dead space. */
.cb-band > .cb-opt__hint:last-child { margin-bottom: -2px; }

.cb-sched { display: flex; flex-direction: column; gap: 6px; }
.cb-sched__head { display: flex; justify-content: space-between; align-items: center;
  font-size: 13.5px; font-weight: 600; color: var(--muted); }
.cb-sched__head button { font: inherit; font-size: 12px; font-weight: 700; border: 0;
  background: var(--cream); color: var(--muted); border-radius: 8px; padding: 4px 10px; cursor: pointer; }
/* wrap, so the quiz-only detail line can drop below the row it describes. */
.cb-sched__row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 11.5px; color: var(--muted); }
.cb-sched__row label { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
/* THE hidden ATTRIBUTE LOSES TO ANY AUTHOR display RULE. syncKind() in course-builder.js sets
   .hidden on the quiz-shape fields and the marks label when the kind is a deck, a game, an
   announcement or a material — and the two rules above and below hand those elements an explicit
   display, which beats the UA's [hidden]{display:none}. The JS was right, the screen ignored it:
   a teacher who chose "Flashcard deck" still saw marks and multiple-choice counts, and read the
   whole row as "it always acts as a quiz" (Victor, 2026-08-26, from the screen). These two put
   the attribute back in charge for exactly the elements syncKind manages. */
.cb-sched__row label[hidden] { display: none; }
.cb-sched__q[hidden] { display: none; }
.cb-sched__g { flex: 1 0 100%; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.cb-sched__g input { width: 58px; }
.cb-sched__g[hidden] { display: none; }   /* born with the guard its sibling had to learn */
/* The quiz-only line. Full width under the row it belongs to, indented so it reads as a detail of
   that row rather than a second scheduled item. */
.cb-sched__q { flex: 1 0 100%; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 6px 0 0 2px; padding-left: 10px; border-left: 2px solid var(--line-2, #efeeea);
  font-size: 12.5px; color: var(--muted); }
.cb-sched__q input { width: 58px; }
.cb-sched__row input[type="number"] { width: 56px; flex: none; padding: 6px 4px; text-align: center; }
.cb-sched__row select { padding: 6px 8px; }
/* The week select must never be squeezed. The row is flex, the kind select is greedy, and a
   shrunk select clips its text from the RIGHT — so "Week 1" became "W" and the only part that
   carried meaning was the first thing lost. flex:none plus room for two digits and the arrow. */
.cb-sched__row select.cb-sched__week { flex: none; width: auto; min-width: 62px; padding-right: 4px; }
/* The kind select is the one with slack to give. */
.cb-sched__row select.cb-sched__kind { flex: 1 1 auto; min-width: 0; }
.cb-sched__kind { flex: 1 1 auto; min-width: 0; }
.cb-sched__rm { border: 0; background: none; cursor: pointer; font-size: 16px; color: #b4bcc4;
  line-height: 1; padding: 2px 4px; }
.cb-sched__rm:hover { color: var(--muted); }

/* THE DOCUMENT SIDE READS AS A PAGE, because that is what it becomes — a handout a parent is given
   and a .docx a teacher opens in Word. A serif measure at ~68 characters is what the exported file
   looks like, so the editor stops being a form that produces a document and starts being the
   document. */
.cb-stage { min-width: 0; }
/* set above: the stage is the scrolling half */
.cb-stage .cb-edit { border-radius: 16px; padding: 0; overflow: hidden; }
/* ONE HEADER ROW. Back, title, week, state and the save indicator were three stacked bands with
   the title in a tinted box of its own — the least-edited thing on the page was the loudest. */
.cb-stage .cb-edit__bar { display: flex; align-items: center; gap: 10px;
  padding: 10px 16px; border-bottom: 1px solid var(--line-2, #efeeea); }
.cb-stage .cb-back { flex: none; width: 28px; height: 28px; display: grid; place-items: center;
  border-radius: 8px; padding: 0; color: var(--muted-2); }
.cb-stage .cb-back:hover { background: var(--cream); color: var(--ink, var(--ink)); }
.cb-stage .cb-title { flex: 1 1 auto; min-width: 0; margin: 0; padding: 4px 6px;
  font-family: var(--head); font-size: 17px; font-weight: 700; background: none; border: 0;
  border-radius: 7px; color: var(--ink, var(--ink)); }
.cb-stage .cb-title:hover { background: var(--wash); }
.cb-stage .cb-title:focus { background: var(--wash); outline: 2px solid #5F8149; outline-offset: -2px; }
.cb-stage .cb-week { flex: none; font-size: 12px; color: var(--muted-2); white-space: nowrap; }
.cb-stage .cb-save { flex: none; font-size: 11.5px; color: var(--muted-2); min-width: 46px; text-align: right; }
.cb-stage .cb-stale { margin: 0; padding: 8px 16px; }

/* THE RIBBON. Grouped with hairline dividers, which is what makes it read as a word processor
   rather than a row of links. Sticks while the document scrolls — a formatting control you have to
   scroll back up to reach is one a teacher stops using. */
.cb-stage .cb-tools { position: sticky; top: 0; z-index: 2; display: flex; align-items: center;
  gap: 2px; margin: 0; padding: 6px 14px; background: var(--wash);
  border-bottom: 1px solid var(--line-2, #efeeea); flex-wrap: wrap; }
.cb-tools__g { display: flex; align-items: center; gap: 2px; padding-right: 8px; margin-right: 6px;
  border-right: 1px solid #e4e9e4; }
.cb-tools__g:last-child { border-right: 0; margin-right: 0; padding-right: 0; }
.cb-stage .cb-tools button { display: grid; place-items: center; width: 36px; height: 34px;
  border: 0; background: none; border-radius: 7px; cursor: pointer; font: inherit; font-size: 13.5px;
  color: var(--ink); }
.cb-stage .cb-tools button svg { width: 22px; height: 22px; display: block; }
.cb-stage .cb-tools button:hover { background: var(--cream); color: var(--ink, var(--ink)); }
.cb-stage .cb-tools button.is-on { background: #dfe8db; color: var(--muted); }
.cb-style, .cb-font { font: inherit; font-size: 12.5px; border: 1px solid var(--line-2, #efeeea);
  border-radius: 7px; padding: 5px 8px; background: var(--bg); min-width: 132px; cursor: pointer; }
/* Narrower than the style box: the names are short and it sits first, so the toolbar does not
   open with two identical grey rectangles. */
.cb-font { min-width: 124px; }
.cb-stage .cb-rte {
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  font-size: 16px; line-height: 1.7; color: var(--ink);
  background: var(--bg); border: 0; border-top: 1px solid var(--line-2, #efeeea);
  padding: 26px clamp(18px, 4%, 44px) 34px; max-width: none; min-height: 440px; }
.cb-stage .cb-rte h2 { font-family: var(--head); font-size: 21px; margin: 22px 0 8px; }
.cb-stage .cb-rte h3 { font-family: var(--head); font-size: 17px; margin: 18px 0 6px; }
.cb-stage .cb-rte p { margin: 0 0 12px; max-width: 82ch; }
.cb-stage .cb-rte ul, .cb-stage .cb-rte ol { max-width: 82ch; margin: 0 0 12px; padding-left: 24px; }
.cb-stage .cb-rte blockquote { max-width: 82ch; margin: 0 0 14px; padding: 2px 0 2px 16px;
  border-left: 3px solid #6f7e71; color: var(--muted); }
.cb-stage .cb-rte hr { border: 0; border-top: 1px solid #6f7e71; margin: 20px 0; max-width: 82ch; }
.cb-stage .cb-rte s { text-decoration: line-through; }
/* ---- The stage, sectioned ---------------------------------------------------
   Everything below the document used to run together in one column: a redraft field, a grey slab
   of assignment, a second redraft field, then the buttons. Four different things, no edges between
   them, and the eye had to work out where each one ended.
   
   Each is now its own bounded block on a tinted ground, so the document reads as the page and
   everything after it reads as the controls for that page. */
.cb-stage .cb-after { background: var(--wash); border-top: 1px solid var(--line-2, #efeeea);
  padding: 14px 16px 16px; display: grid; gap: 14px; }
/* .cb-asg IS NOW A BARE WRAPPER. It used to be the card; the collapsible block inside it is the
   card now, and leaving both styled drew two borders around one thing. It survives only because
   paintAssignment toggles its `hidden` to swap in the "no work to hand in" panel. */
.cb-stage .cb-asg { margin: 0; background: none; border: 0; padding: 0; }
.cb-stage .cb-asg-none {
  margin: 0; background: var(--bg); border: 1px solid var(--line-2, #efeeea);
  border-radius: 14px; padding: 14px 16px; }
/* The document block carries the page, so it sits in the same gutter as everything below it. */
.cb-stage #cb-blk-body { margin: 0 16px 12px; }
/* LIGHTER FIELDS INSIDE A WHITE CARD. #E7ECE8 was chosen when this block sat on the page ground
   and needed to read as editable; inside a white card the same tint reads as three grey slabs and
   fights the card for attention. A hairline does the same job of saying "you can type here"
   without the weight, and the tint comes back on focus where it actually means something. */
.cb-stage .cb-asg__title, .cb-stage .cb-asg__body, .cb-stage .cb-asg__pts input {
  background: var(--wash); border: 1px solid var(--line-2, #efeeea); }
.cb-stage .cb-asg__title:focus, .cb-stage .cb-asg__body:focus, .cb-stage .cb-asg__pts input:focus {
  background: var(--bg); border-color: var(--green-ink); box-shadow: 0 0 0 3px var(--green-tint);
  outline: none; }
.cb-stage .cb-edit__foot { padding: 14px 16px 16px; margin: 0;
  border-top: 1px solid var(--line-2, #efeeea); background: var(--wash); }

/* ---- A session as a short list of what is in it -----------------------------
   Everything attached to a session used to be laid out full length under the document, so reading
   one meant scrolling past all of it and the teacher who only had time for the first three read
   the first three. Collapsed, a session is a list of its pieces, and each summary line carries
   what it is, what state it is in, and what changing it would cost. */
.cb-stage .cb-blk { border: 1px solid var(--line-2, #efeeea); border-radius: 14px; background: var(--bg);
  overflow: hidden; }
.cb-stage .cb-blk + .cb-blk { margin-top: 12px; }
.cb-blk__h { display: flex; align-items: center; gap: 10px; padding: 12px 14px; cursor: pointer;
  list-style: none; user-select: none; }
/* The default triangle sits outside the padding box and cannot be aligned; a rotating chevron of
   our own can. */
.cb-blk__h::-webkit-details-marker { display: none; }
.cb-blk__h::before { content: ""; flex: none; width: 8px; height: 8px; margin-right: 2px;
  border-right: 2px solid #8a939d; border-bottom: 2px solid #8a939d;
  transform: rotate(-45deg); transition: transform .18s ease; }
.cb-blk[open] > .cb-blk__h::before { transform: rotate(45deg); }
.cb-blk__h:hover { background: var(--wash); }
.cb-blk__t { font-family: var(--head); font-size: 14px; font-weight: 700; flex: 1 1 auto; min-width: 0; }
.cb-blk__meta { flex: none; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  justify-content: flex-end; }
.cb-blk[open] > .cb-blk__h { border-bottom: 1px solid var(--line-2, #efeeea); }
/* The document block IS the page, so it gets no inner padding — the ribbon and the paper run to
   its edges exactly as they did when there was no block around them. */
#cb-blk-body > .cb-blk__body { padding: 0; }
#cb-blk-asg > .cb-blk__body { padding: 14px 16px; }
.cb-chip.is-asg { background: var(--cream); color: var(--muted); }

/* WHAT IT COSTS TO CHANGE THIS, on the row you decide from. */
.cb-uses { flex: none; font-size: 11.5px; font-weight: 700; color: var(--muted);
  background: var(--cream); border-radius: 999px; padding: 4px 10px; white-space: nowrap; }
.cb-uses.is-pool { background: var(--cream); color: var(--muted); }
.cb-uses.is-out { background: var(--cream); color: #9a7a76; }

/* The composer, folded away. It is the least-used control under any piece and it was the most
   prominent thing there — a standing invitation to regenerate work not yet read. */
/* The composer strip. No disclosure any more — the field is simply there, at the bottom of the
   piece it rewrites. */
.cb-pop { border-top: 1px solid var(--line-2, #efeeea); background: var(--wash); padding: 14px 16px; }
#cb-blk-asg .cb-pop { margin: 14px -16px -14px; }

/* THE SHARED BUDGET, before any of it is spent. Per-piece counts sit beside the thing they belong
   to, which is right, but a number that only ever appears inside session one is one a teacher
   meets for the first time when it has already gone. */
/* On the LIST it aligns with the rows; inside a session it sits in the editor's gutter. Same
   component, two containers, and one margin cannot serve both. */
.cb-pool { margin: 0 0 14px; padding: 10px 13px; border-radius: 11px; font-size: 12.5px;
  line-height: 1.5; color: var(--muted); background: var(--wash);
  border: 1px solid var(--line-2, #efeeea); }
.cb-pool b { color: var(--ink, var(--ink)); }
.cb-stage .cb-pool { margin: 12px 16px; }
.cb-pool.is-out { background: var(--cream); border-color: #f0e2bb; color: var(--muted); }
.cb-pool.is-out b { color: var(--muted); }
.cb-asg__rmrow { margin: 12px 0 0; }

/* ---- Adding to a course that already exists ---------------------------------
   Folded away, because it is the thing you reach for AFTER reading, and on first arrival the job
   is to read. Open it and it is the same three questions the scheduler asked before drafting —
   what, where, worth how much — except "where" now names real sessions with real titles instead
   of a week number nothing has been written into yet. */
.cb-add { border: 1px solid var(--line-2, #efeeea); border-radius: 14px; background: var(--bg);
  margin: 0 0 14px; overflow: hidden; }
/* Inside a session it sits in the editor's gutter, above the shared-budget line. */
.cb-stage .cb-add { margin: 12px 16px; }
/* One row, ending in the button — "Add a Quiz, marks 100, Write it" reads as a sentence, which is
   the whole reason the session picker could go: the session is in the summary above it. */
.cb-add__go { margin-left: auto; border: 0; border-radius: 9px; padding: 8px 16px;
  font: 700 12.5px var(--sans); color: #fff; cursor: pointer;
  background: linear-gradient(120deg, #7C3AED, #A855F7 55%, #22C55E);
  background-size: 180% 100%; transition: background-position .35s ease; }
.cb-add__go:hover:not(:disabled) { background-position: 100% 0; }
.cb-add__go:disabled { background: #b9c2b3; cursor: default; }
.cb-add__h { display: flex; align-items: center; gap: 8px; padding: 12px 16px; cursor: pointer;
  list-style: none; font-family: var(--head); font-size: 13.5px; font-weight: 700;
  color: var(--purple-ink, var(--muted)); }
.cb-add__h::-webkit-details-marker { display: none; }
.cb-add__h:hover { background: var(--purple-tint, var(--cream)); }
.cb-add[open] > .cb-add__h { border-bottom: 1px solid var(--line-2, #efeeea); }
.cb-add__body { padding: 14px 16px; display: grid; gap: 10px; background: var(--wash); }
.cb-add__row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--muted); }
.cb-add__row label { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
.cb-add__row select, .cb-add__row input { font: inherit; font-size: 13px;
  border: 1px solid var(--line-2, #efeeea); border-radius: 8px; padding: 6px 9px; background: var(--bg); }
/* The session picker carries a title, so it is the one control here that needs room. */
#cb-add-session { max-width: 340px; }
.cb-add__row input[type="number"] { width: 66px; }
/* DETACHED. This carried an indent rail, which read as "these belong to the control above" —
   true when only a quiz had options, misleading now every kind has its own row. A hairline
   separator says "same panel, next question" without implying ownership. */
.cb-add__opt { padding-top: 10px; border-top: 1px solid var(--line-2, #efeeea); }
.cb-add__opt input[type="number"] { width: 76px; }
.cb-add__n { font-size: 12px; color: var(--muted); }
.cb-add__err { margin: 0; font-size: 12.5px; color: var(--muted); }
/* The running total. Grey while it fits, red the moment it does not, so the cap is met while
   typing rather than discovered in the result. */
.cb-add__tot { font-size: 12px; color: var(--muted); }
.cb-add__tot.is-over { color: var(--muted); font-weight: 700; }

/* ---- Something is being built ----------------------------------------------
   The add call is the slowest control on this page, and a greyed button reading "Writing…" is the
   same picture as a button that did nothing. This is the shape of the block that is coming,
   shimmering in the place it will appear — the page promises what was asked for and fills it in,
   instead of standing still and then jumping. */
.cb-ghost { border: 1px dashed var(--purple-light, #cabff2); border-radius: 14px;
  background: var(--bg); padding: 14px 16px; margin-top: 12px;
  animation: cbrise .3s ease both; }
.cb-ghost__h { display: flex; align-items: center; gap: 10px; }
.cb-ghost__lab { font-size: 12.5px; font-weight: 700; color: var(--purple-ink, var(--muted));
  text-transform: capitalize; }
.cb-ghost__b { display: grid; gap: 8px; margin-top: 12px; }
/* The skeletons stagger, so it reads as work moving through rather than one slab pulsing. */
.cb-ghost__b .cb-live__sk:nth-child(2) { animation-delay: .12s; }
.cb-ghost__b .cb-live__sk:nth-child(3) { animation-delay: .24s; width: 40%; }
@media (prefers-reduced-motion: reduce) { .cb-ghost { animation: none; } }
.cb-add [hidden] { display: none !important; }

/* ---- The quiz, where a mistake marks a child --------------------------------
   The correct option has to be readable at a glance down the whole quiz, and a question with none
   chosen has to look wrong rather than merely blank — it is the one thing on this page that is
   applied silently to every student. */
.cb-quiz__lead { font-size: 12.5px; line-height: 1.5; color: var(--muted); margin: 0 0 12px;
  padding: 9px 12px; border-radius: 10px; background: var(--green-tint, var(--cream)); }
.cb-quiz__lead.is-bad { background: var(--cream); color: var(--muted); }
.cb-quiz { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.cb-q { border: 1px solid var(--line-2, #efeeea); border-radius: 12px; padding: 12px; background: var(--bg); }
.cb-q.is-bad { border-color: #e5b4ae; background: var(--bg); }
.cb-q__top { display: flex; align-items: center; gap: 8px; }
.cb-q__n { flex: none; width: 22px; height: 22px; display: grid; place-items: center;
  border-radius: 999px; background: var(--purple-tint, var(--cream)); color: var(--purple-ink, var(--muted));
  font-size: 11.5px; font-weight: 800; }
.cb-q__prompt { flex: 1 1 auto; min-width: 0; font: inherit; font-size: 14px; font-weight: 600;
  border: 1px solid transparent; border-radius: 8px; padding: 6px 8px; background: var(--wash); }
.cb-q__prompt:focus { outline: none; border-color: var(--green-ink); background: var(--bg); }
.cb-q__pts { flex: none; display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--muted); }
.cb-q__pts input { width: 54px; font: inherit; font-size: 12.5px; border: 1px solid var(--line-2, #efeeea);
  border-radius: 7px; padding: 5px 7px; background: var(--wash); }
.cb-q__opts { display: grid; gap: 6px; margin: 10px 0 0 30px; }
.cb-q__opt { display: flex; align-items: center; gap: 9px; border-radius: 9px; padding: 3px 6px 3px 3px; }
/* THE ANSWER, GREEN AND OBVIOUS. Scanning twenty questions for which radio is filled is exactly
   the sort of check a tired teacher skips. */
.cb-q__opt.is-right { background: var(--green-tint, var(--cream)); }
.cb-q__opt input[type="radio"] { flex: none; width: 16px; height: 16px; accent-color: var(--green-ink, var(--muted)); }
.cb-q__ctext { flex: 1 1 auto; min-width: 0; font: inherit; font-size: 13.5px;
  border: 1px solid transparent; border-radius: 7px; padding: 5px 7px; background: transparent; }
.cb-q__ctext:focus { outline: none; border-color: var(--green-ink); background: var(--bg); }
.cb-q__opt.is-right .cb-q__ctext { font-weight: 700; color: var(--muted); }
.cb-q__short { display: flex; align-items: center; gap: 8px; margin: 10px 0 0 30px;
  font-size: 12px; color: var(--muted); }
.cb-q__short input { flex: 1 1 auto; min-width: 0; font: inherit; font-size: 13.5px;
  border: 1px solid var(--line-2, #efeeea); border-radius: 8px; padding: 6px 8px; background: var(--wash); }
.cb-q__short input:focus { outline: none; border-color: var(--green-ink); background: var(--bg); }
.cb-q__warn { margin: 8px 0 0 30px; font-size: 12px; color: var(--muted); }
.cb-chip.is-quiz { background: var(--purple-tint, var(--cream)); color: var(--purple-ink, var(--muted)); }
.cb-chip.is-x { background: var(--wash); color: var(--muted); }
/* The extras: a title where there is one, and the text. Plain fields, because these are short
   pieces of plain prose — the rich editor above belongs to the document, and offering headings
   and lists on a two-sentence announcement invites formatting that the pod strips anyway. */
.cb-x__title { width: 100%; font: inherit; font-size: 15px; font-weight: 600; margin: 0 0 8px;
  border: 1px solid var(--line-2, #efeeea); border-radius: 9px; padding: 9px 11px; background: var(--wash); }
.cb-x__text { width: 100%; font: inherit; font-size: 14px; line-height: 1.6; resize: vertical;
  border: 1px solid var(--line-2, #efeeea); border-radius: 9px; padding: 10px 12px; background: var(--wash); }
.cb-x__title:focus, .cb-x__text:focus { outline: none; border-color: var(--green-ink);
  box-shadow: 0 0 0 3px var(--green-tint); background: var(--bg); }
[id^="cb-blk-x"] > .cb-blk__body { padding: 14px 16px; }
#cb-blk-quiz > .cb-blk__body { padding: 14px 16px; }

/* HIDDEN MUST WIN. .cb-asg-none sets `display: flex`, and an author rule beats the UA stylesheet's
   [hidden] { display: none } — so the "No work to hand in for this session" panel was drawn at all
   times, including directly underneath an assignment that plainly existed. paintAssignment was
   setting .hidden correctly the whole time and nothing was listening. */
.cb-edit [hidden] { display: none !important; }

/* THE SIDE-BY-SIDE WORKSPACE IS THE POINT, so it holds much further down than it used to.
   Stacking began at 1080px, which sounds wide and is not: a laptop with a terminal or a chat
   panel open beside the browser sits around 1000px, and there the drafted documents dropped
   BELOW the form — a full screen down, out of sight, exactly where a teacher watching a draft
   run is not looking.
   
   With the rail on a clamp it now holds to 700px, where a 268 rail plus 16 of gap still leaves
   ~400px of document. Below that the two columns really are worse than one. Measured at 800,
   760 and 690, not guessed — the previous two numbers here were guesses and both were wrong. */
@media (max-width: 700px) {
  /* Stacked, so a fixed-height two-column workspace makes no sense — back to page flow. */
  .cb-work { grid-template-columns: minmax(0, 1fr); height: auto; align-items: start; }
  .cb-rail, .cb-stage { overflow: visible; }
  .cb-rail { position: static; max-height: none; }
  .cb-rail__foot { position: static; bottom: auto; }
}

/* WHAT TO DO, IN ORDER. Nothing said the sessions had to be opened one at a time, so the only way
   to learn it was to publish one and notice the rest had not moved. */
/* Scoped page-chrome trim: the Pod Builder is a workspace, not an article. */
/* The page pads 24-40px above the title, the title block adds ~45 more, and .dash__main leaves a
   64px tail below — around a workspace whose whole point is that nothing scrolls. All three are
   trimmed for this view only; every other view keeps the chrome it was designed with. Measured
   rather than guessed: the workspace started 69px down and the rail needed 42 more than it had. */
.vsection--wide { margin-top: calc(-1 * clamp(14px, 2vw, 30px)); margin-bottom: 0; }
.vsection--wide .vsection__head { margin-bottom: 8px; }
.vsection--wide .vsection__head h2 { font-size: 19px; }
/* The credit is a footnote, and a footnote must not be the reason a workspace scrolls. Taken out
   of flow so it costs no height — the rail ends well above it and the document pane scrolls
   underneath its own rounded corner, so nothing is ever hidden behind it. */
.vsection--wide { position: relative; }
.vsection--wide .cb-credit { position: absolute; left: 0; bottom: -2px; margin: 0;
  font-size: 11px; pointer-events: none; }
/* The tail is for scrolling pages. This one does not scroll. */
.dash__main:has(.vsection--wide) { padding-bottom: 12px;
  /* THE CARD, ~15% LARGER. Victor asked for the whole Pod Builder card bigger. The dashboard
     column caps at 1600px; this view alone goes to 1840 (1600 x 1.15) so the card fills more of a
     wide screen instead of sitting islanded in whitespace. Interior type and the rail width step up
     with it below, so it reads as larger in general rather than merely wider. Scoped to this view
     with :has() — no other dashboard page changes. */
  max-width: 1840px; }
.vsection--wide .vsection__sub { margin-top: 2px; }

.cb-steps { list-style: none; counter-reset: s; margin: 0 0 14px; padding: 12px 14px;
  background: var(--wash); border-radius: 12px; display: grid; gap: 6px; }
.cb-steps li { counter-increment: s; position: relative; padding-left: 26px;
  font-size: 13px; color: var(--muted); line-height: 1.5; }
.cb-steps li::before { content: counter(s); position: absolute; left: 0; top: 1px;
  width: 18px; height: 18px; border-radius: 50%; background: #5F8149; color: #fff;
  font-size: 11px; font-weight: 700; display: grid; place-items: center; }
.cb-listbar__acts { display: flex; gap: 8px; flex: none; flex-wrap: wrap; }
.cb-listbar__acts .btn[disabled] { opacity: .5; cursor: not-allowed; }

/* THE TEACHER'S OWN RULE, BROKEN. Amber rather than red: this is "read this before you approve",
   not "something failed". The check can be wrong and the teacher knows their pod. */
.cb-forbid { margin: 12px 16px 0; padding: 12px 14px; border-radius: 12px;
  background: var(--cream); border: 1px solid #f0dfae; }
.cb-forbid.is-ack { background: var(--cream); border-color: #e6eae6; }
.cb-forbid__head { font-weight: 700; font-size: 13.5px; color: var(--muted); margin-bottom: 4px; }
.cb-forbid.is-ack .cb-forbid__head { color: var(--muted); }
.cb-forbid__rule { font-size: 12.5px; color: var(--muted); margin: 0 0 6px; }
.cb-forbid__quote { font-family: Georgia, serif; font-size: 13.5px; color: var(--ink);
  margin: 0 0 6px; padding-left: 10px; border-left: 3px solid #e2b23c; }
.cb-forbid p { font-size: 12.5px; color: var(--muted); margin: 0 0 4px; line-height: 1.5; }
.cb-forbid__act { margin-top: 8px !important; }
.cb-forbid__act button { font: inherit; font-size: 12.5px; font-weight: 700; border: 0;
  background: #f0e4c0; color: var(--muted); border-radius: 7px; padding: 4px 10px; cursor: pointer; }
.cb-forbid__act button:hover { background: #e8d7a8; }
/* Visible in the list too, so a flagged session is findable without opening all of them. */
.cb-flag { flex: none; display: grid; place-items: center; width: 18px; height: 18px;
  border-radius: 50%; background: #f0dfae; color: var(--muted); font-size: 11.5px; font-weight: 800; }

.cb-progress { font-size: 13.5px; color: var(--muted); margin: 0; }

/* HOW FAR ALONG, AS A BAR. The count was a 13.5px muted sentence — fine-print treatment for the
   one number that answers "am I nearly done". It also does not survive scale: at four sessions
   "2 of 4" reads fine, at twenty-four nobody counts. The bar does the same job at any length. */
.cb-prog { display: flex; flex-direction: column; gap: 7px; min-width: 220px; flex: 1 1 260px; }
.cb-prog__line { margin: 0; font-size: 13.5px; color: var(--muted); }
.cb-prog__line b { color: var(--ink, var(--ink)); font-size: 15px; }
.cb-prog__hint { display: block; font-size: 12.5px; margin-top: 2px; }
.cb-prog__track { display: block; height: 6px; border-radius: 999px; background: var(--wash); overflow: hidden; }
.cb-prog__fill { display: block; height: 100%; border-radius: 999px; background: #5F8149;
  transition: width .35s ease; }

/* A COURSE IS A SEQUENCE, so the list is ordered and carries a number rail. Twenty-four identical
   cards give a teacher nothing to count against; the numbers are what let them find session 17. */
.cb-list { list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: minmax(0, 1fr); gap: 8px; }
.cb-row { position: relative; }
.cb-item { display: flex; align-items: center; gap: 14px;
  width: 100%; text-align: left; font: inherit; cursor: pointer; background: var(--bg); border: 0;
  border-radius: 14px; padding: 13px 16px 13px 14px;
  box-shadow: 0 1px 2px rgba(20,21,26,.06), 0 8px 24px rgba(20,21,26,.05);
  transition: transform .16s ease, box-shadow .16s ease, background .16s ease; }
.cb-item:hover { transform: translateY(-1px); background: var(--bg);
  box-shadow: 0 2px 4px rgba(20,21,26,.07), 0 12px 30px rgba(20,21,26,.08); }
.cb-item:focus-visible { outline: 2px solid #5F8149; outline-offset: 2px; }

/* STATE ON THE EDGE, so it can be scanned down the column rather than read one chip at a time. */
.cb-item::before { content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px;
  border-radius: 999px; background: #dfe4e9; }
.cb-row.is-approved .cb-item::before { background: #e2b23c; }
.cb-row.is-published .cb-item::before { background: #5F8149; }

/* The rail. Tabular figures so 9 and 10 line up. */
.cb-item__n { flex: none; width: 26px; text-align: right; font-size: 13px; font-weight: 700;
  color: #aeb6bf; font-variant-numeric: tabular-nums; }
.cb-row.is-published .cb-item__n { color: var(--muted); }

.cb-item__main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
.cb-item__main b { font-size: 15px; }
.cb-item__title { font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* These rows were buttons all along and looked like inert cards — the copy says "approve them one
   at a time" while nothing on the row suggested it could be pressed. */
.cb-item__go { flex: none; font-size: 20px; line-height: 1; color: #c3cad2;
  transition: transform .16s ease, color .16s ease; }
.cb-item:hover .cb-item__go { transform: translateX(2px); color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  .cb-item, .cb-item__go, .cb-prog__fill { transition: none; }
  .cb-item:hover { transform: none; }
  .cb-item:hover .cb-item__go { transform: none; }
}
.cb-chip { flex: none; font-size: 11.5px; font-weight: 700; padding: 4px 10px; border-radius: 999px; white-space: nowrap; }
.cb-chip.is-draft { background: var(--wash); color: var(--muted); }
.cb-chip.is-approved { background: var(--cream); color: var(--muted); }
.cb-chip.is-published { background: var(--cream); color: var(--muted); }

.cb-edit { background: var(--bg); border-radius: 16px; padding: 18px 20px 20px;
  box-shadow: 0 1px 2px rgba(20,21,26,.06), 0 10px 30px rgba(20,21,26,.06); }
.cb-edit__bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.cb-back { display: inline-flex; align-items: center; gap: 6px; background: none; border: 0;
  padding: 6px 2px; font: inherit; font-size: 13.5px; color: var(--muted); cursor: pointer; border-radius: 8px; }
.cb-back:hover { color: var(--ink); }
.cb-save { font-size: 12.5px; color: var(--muted); }
.cb-save.is-good { color: var(--muted); font-weight: 600; }
.cb-save.is-bad { color: var(--muted); font-weight: 600; }
.cb-title { width: 100%; font: inherit; font-size: 20px; font-weight: 700; margin: 10px 0 2px;
  background: var(--line); border: 0; border-radius: 10px; padding: 10px 12px; }
.cb-title:focus { background: #DDE4DE; outline: none; }
.cb-week { font-size: 13px; color: var(--muted); display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.cb-stale { font-size: 13px; background: var(--cream); color: var(--muted); border-radius: 10px; padding: 9px 12px; margin: 0 0 12px; }
.cb-tools { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }
.cb-tools button { font: inherit; font-size: 13px; background: var(--wash); border: 0; border-radius: 8px;
  padding: 6px 11px; cursor: pointer; color: var(--ink); }
.cb-tools button:hover { background: #e2e7ec; }
.cb-rte { min-height: 320px; background: var(--line); border-radius: 12px; padding: 16px 18px;
  font-size: 15px; line-height: 1.65; overflow-y: auto; max-height: 60vh; }
.cb-rte:focus { background: #DDE4DE; outline: none; }
.cb-rte h2 { font-size: 19px; margin: 18px 0 6px; }
.cb-rte h3 { font-size: 16px; margin: 16px 0 5px; }
.cb-rte p { margin: 0 0 10px; }
.cb-rte ul, .cb-rte ol { margin: 0 0 12px; padding-left: 22px; }
.cb-rte li { margin: 0 0 5px; }
.cb-rte blockquote { margin: 0 0 12px; padding: 2px 0 2px 14px; border-left: 3px solid #b9c6bb;
  color: var(--muted); }
.cb-rte hr { border: 0; border-top: 1px solid #b9c6bb; margin: 16px 0; }
.cb-rte s { text-decoration: line-through; }
/* The assignment: a distinct block, because it publishes as a distinct thing. */
.cb-asg { background: var(--wash); border-radius: 12px; padding: 14px 16px; margin: 14px 0 0; }
.cb-asg__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.cb-asg__head h3 { font-size: 14px; margin: 0 0 2px; }
.cb-asg__rm { background: none; border: 0; font: inherit; font-size: 12.5px; color: var(--muted);
  cursor: pointer; text-decoration: underline; text-underline-offset: 3px; padding: 4px; border-radius: 6px; }
.cb-asg__rm:hover { color: var(--ink); }
.cb-asg__title { width: 100%; font: inherit; font-size: 15px; font-weight: 600; margin: 8px 0;
  background: var(--line); border: 0; border-radius: 9px; padding: 9px 11px; }
.cb-asg__title:focus { background: #DDE4DE; outline: none; }
.cb-asg__body { min-height: 96px; background: var(--line); border-radius: 9px; padding: 11px 13px;
  font-size: 14.5px; line-height: 1.6; }
.cb-asg__body:focus { background: #DDE4DE; outline: none; }
.cb-asg__body p { margin: 0 0 8px; }
.cb-asg__body ul, .cb-asg__body ol { margin: 0 0 8px; padding-left: 20px; }
.cb-asg__pts { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); margin: 10px 0 0; }
.cb-asg__pts input { width: 72px; font: inherit; background: var(--line); border: 0;
  border-radius: 8px; padding: 6px 9px; }
.cb-asg__hint { font-size: 12px; color: var(--muted); }
.cb-asg-none { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; background: var(--wash); border-radius: 12px; padding: 13px 16px; margin: 14px 0 0;
  font-size: 13.5px; color: var(--muted); }
.cb-asg-none button { background: none; border: 0; font: inherit; font-size: 13.5px;
  color: var(--muted); cursor: pointer; text-decoration: underline; text-underline-offset: 3px;
  padding: 4px; border-radius: 6px; }

.cb-err { font-size: 13px; color: var(--muted); background: var(--cream); border-radius: 10px;
  padding: 9px 12px; margin: 10px 0 0; }
.cb-edit__foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 16px 0 0; }
.cb-foot-note { font-size: 12.5px; color: var(--muted); margin: 0; }
@media (max-width: 560px) {
  .cb-item { flex-direction: column; align-items: flex-start; gap: 8px; }
  .cb-rte { min-height: 240px; }
}

/* The one piece of STATE on the Orders page: what you are on, and the way out of it. */
/* THE SAME SHADOW AS A PAYMENT ROW. It used to carry a big lifted one while the rows sat flat,
   so a page with two blocks on it read as two different card systems stacked up. The card is
   distinguished by WHAT IT SAYS and by the "Payments" label under it, not by floating higher. */
.ord__pkg { display: flex; align-items: center; justify-content: space-between; gap: 16px;
  background: var(--bg); border-radius: 14px; padding: 16px 18px; margin: 0 0 24px;
  box-shadow: 0 1px 2px rgba(16,24,40,.06), 0 1px 3px rgba(16,24,40,.04); }
.ord__pkg-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ord__pkg-main b { font-size: 16px; }
.ord__pkg-sub { font-size: 13px; color: var(--muted); }
/* A card failing is not a card ending: past_due shades amber, cancelled-but-running stays calm,
   because the teacher still has everything they paid for until the period closes. */
.ord__pkg.is-past-due { background: var(--cream); }
.ord__pkg.is-past-due .ord__pkg-sub { color: var(--muted); font-weight: 600; }
.ord__pkg.is-ending .ord__pkg-sub { color: var(--muted); }
/* Quiet, not red. Cancelling is a legitimate thing a paying customer may want to do, and
   dressing it as a danger is how a product makes leaving feel like a transgression. */
.ord__pkg-cancel { flex: none; background: none; border: 0; padding: 8px 4px; cursor: pointer;
  font: inherit; font-size: 13.5px; color: var(--muted); text-decoration: underline;
  text-underline-offset: 3px; border-radius: 8px; }
.ord__pkg-cancel:hover { color: var(--ink); }
.ord__pkg-cancel:focus-visible { outline: 2px solid #5F8149; outline-offset: 2px; }
@media (max-width: 560px) {
  .ord__pkg { flex-direction: column; align-items: flex-start; gap: 10px; }
}

/* [work/admin-console-roundout] My Reports tab - filed reports + staff responses. */
.rpt-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; max-width: 720px; }
.rpt { background: var(--bg); border: 1px solid var(--line, #e5e4e0); border-radius: 14px; padding: 16px 18px; }
.rpt__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.rpt__top strong { font-size: 15px; }
.rpt-pill { font-size: 11.5px; font-weight: 700; padding: 4px 10px; border-radius: 999px; white-space: nowrap; }
.rpt-pill--open { background: var(--cream); color: var(--muted); }
.rpt-pill--ok { background: var(--cream); color: var(--muted); }
.rpt-pill--dim { background: var(--cream); color: var(--muted); }
.rpt__meta { color: var(--muted, var(--muted)); font-size: 12.5px; margin: 4px 0 8px; }
.rpt__reason { color: var(--muted); font-size: 13.5px; line-height: 1.5; margin: 0; }
.rpt__wait { color: var(--muted, var(--muted)); font-size: 12.5px; margin: 10px 0 0; }
.rpt__resp { margin-top: 12px; background: var(--purple-tint, var(--cream)); border-radius: 10px; padding: 12px 14px; }
.rpt__resp-h { display: block; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--purple-ink, var(--muted)); margin-bottom: 4px; }
.rpt__resp p { margin: 0; color: var(--ink); font-size: 13.5px; line-height: 1.5; }
.rpt__resp small { display: block; margin-top: 6px; color: var(--purple-ink, var(--muted)); font-size: 11.5px; font-weight: 600; }

/* [work/admin-console-roundout] Parent payments: autopay manager bar + cancel note. */
.pay-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0 0 14px; }
.pay-actions__hint { color: var(--muted, var(--muted)); font-size: 12.5px; }
.pay-actions__status { color: var(--muted); font-size: 12.5px; font-weight: 600; flex-basis: 100%; }
.pay-actions__status:empty { display: none; }
.pay-note { color: var(--muted, var(--muted)); font-size: 12.5px; margin: 10px 2px 0; line-height: 1.5; }
/* Formal-receipt download button, shown right of the pod name in the parent payments table. */
.rcpt-btn { display: inline-flex; align-items: center; gap: 4px; margin-left: 10px; padding: 3px 10px;
  font: 600 12px inherit; color: var(--purple-ink, var(--muted)); background: var(--purple-tint, var(--cream));
  border: 1px solid transparent; border-radius: 999px; cursor: pointer; vertical-align: middle;
  transition: background .15s, border-color .15s; white-space: nowrap; }
.rcpt-btn:hover { background: #e7dbfa; border-color: #d5c4f0; }
.rcpt-btn:disabled { opacity: .6; cursor: default; }

/* Past Pods / Records - archived grade records (grade-archive.js → myPastPods). */
.rec-list { display: flex; flex-direction: column; gap: 12px; }
.rec-card { border: 1px solid var(--line); border-radius: 12px; background: var(--bg); overflow: hidden; }
.rec-card__head { display: flex; align-items: center; gap: 14px; padding: 14px 16px; cursor: pointer; }
.rec-card__head:hover { background: var(--bg); }
.rec-card__id { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.rec-card__id strong { font-size: 15px; }
.rec-card__id small { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
.rec-grade { flex: 0 0 auto; font-weight: 800; font-size: 17px; color: var(--green-ink, var(--muted)); }
.rec-grade small { font-weight: 600; font-size: 12px; color: var(--muted); }
.rec-grade--none { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.rec-card__chev { color: var(--muted); transition: transform .18s; flex: 0 0 auto; }
.rec-card__head.is-open .rec-card__chev { transform: rotate(180deg); }
.rec-card__body { padding: 4px 16px 14px; border-top: 1px solid var(--line-2, #efeeea); }
.rec-asg { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line-2, #efeeea); font-size: 13.5px; }
.rec-asg:last-child { border-bottom: 0; }
.rec-asg__t { color: var(--ink-2, var(--ink)); min-width: 0; }
.rec-asg__g { flex: 0 0 auto; font-weight: 600; font-variant-numeric: tabular-nums; }
.rec-none { color: var(--muted); font-size: 13px; padding: 8px 0; }
/* Panel placeholder while its data loads — matches .panel__empty's height so revealing the
   real rows (or the real empty state) doesn't jump the layout. */
.panel__wait { display: flex; flex-direction: column; justify-content: center; gap: 14px; min-height: 190px; padding: 24px; }

/* ---- Settings → Security --------------------------------------------------------------
   Blocked list, data export, and account deletion.

   Two house styles fought this screen. `.vsettings .vpanel` sets max-width:none so teacher
   settings cards fill the window — fine for the two-column account grid, unreadable for
   single-column prose. And `.vnote` is a mint-green callout, so three explanatory paragraphs
   became three stacked alert boxes. Security uses quiet helper text and reserves colour for the
   one panel that earns it. */
#sec-security { max-width: 760px; }
#sec-security .vpanel { margin-bottom: 16px; padding: 22px 24px; }
#sec-security .vpanel:last-child { margin-bottom: 0; }

/* Card heading row — title left, optional tag right. */
.sec-head { display: flex; align-items: center; gap: 10px; margin: 0 0 5px; }
.sec-head h3 { margin: 0; font-size: 15.5px; font-weight: 700; }
.sec-tag {
  font-size: 10.5px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); background: var(--wash); border-radius: 5px; padding: 3px 7px;
}

/* Explanatory copy: guidance, not an alert. */
.sec-help { margin: 0 0 16px; color: var(--muted); font-size: 13.5px; line-height: 1.6; }
.sec-help--why { margin: 14px 0 0; font-size: 12.5px; }
.sec-empty { margin: 0; color: var(--muted); font-size: 13.5px; }

.sec-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.sec-actions .vfield-status { margin: 0; }

/* What deletion removes vs what it keeps. The retention model is the point of the panel, and as
   a paragraph it went unread — side by side it lands in a glance. */
.sec-split { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.sec-split__col { background: var(--bg); border: 1px solid #f0dcd6; border-radius: 12px; padding: 13px 15px; }
.sec-split__head {
  margin: 0 0 8px; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
}
.sec-split__head--go { color: var(--muted); }
.sec-split__head--stay { color: var(--muted); }
.sec-split__col ul { margin: 0; padding-left: 16px; color: var(--muted); font-size: 13px; line-height: 1.75; }

.vpanel--danger { border-color: #f0c9c2; background: var(--bg); }
.vpanel--danger h3 { color: var(--muted); }
.vpanel--danger .sec-help { color: var(--muted); }

.btn--danger { background: #c0341c; border-color: var(--muted); color: #fff; }
.btn--danger:hover:not(:disabled) { background: #a8321c; border-color: var(--muted); }
.btn--danger:disabled { opacity: .5; cursor: not-allowed; }

/* Block someone — sits ABOVE the blocked list, because creating a block was the half that
   didn't exist. Bordered off so it reads as an action, not another blocked person. */
.sec-blockadd { padding: 0 0 14px; margin: 0 0 14px; border-bottom: 1px solid var(--line); }
.sec-blockadd__l { display: block; font-size: 13px; font-weight: 600; color: var(--ink); margin: 0 0 6px; }
.sec-blockadd__row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.sec-blockadd__row select {
  flex: 1 1 200px; min-width: 0; padding: 9px 11px; font: inherit; font-size: 14px;
  color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-btn, 12px);
}
.sec-blockadd .vfield-status:empty { display: none; }

.sec-blocklist { list-style: none; margin: 0; padding: 0; }
.sec-blockrow {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--line);
}
.sec-blockrow:first-child { padding-top: 0; }
.sec-blockrow:last-child { border-bottom: 0; padding-bottom: 0; }
.sec-blockrow strong { font-weight: 600; }
.sec-blockrow__who small { display: block; color: var(--muted); margin-top: 2px; }

/* Why a deletion is refused — heading and reasons in ONE block. */
.sec-refusal { background: var(--cream); border-radius: var(--radius-sm); padding: 14px 16px; margin-top: 16px; }
.sec-refusal__head { margin: 0 0 8px; font-size: 13.5px; font-weight: 700; color: var(--muted); }
.sec-blockers { margin: 0; padding-left: 18px; color: var(--muted); font-size: 13.5px; line-height: 1.6; }
.sec-blockers li + li { margin-top: 6px; }

#sec-delete .vfield { margin-top: 16px; }

@media (max-width: 620px) {
  .sec-split { grid-template-columns: 1fr; }
  .sec-blockrow { flex-direction: column; align-items: flex-start; }
}

/* ---------- Account deletion (security-settings.js) ---------- */
/* Consequences are stated BEFORE the confirm box, not after — model B allows deletion mid-pod,
   so the disclosure is the safeguard rather than a blocked button. */
.sec-warn { margin: 0 0 12px; padding: 12px 14px; border-radius: 11px; font-size: 13.5px;
  line-height: 1.55; background: var(--cream); border: 1px solid #f0d9a8; color: var(--muted); }
.sec-warn__h { margin: 0 0 6px; font-weight: 700; }
.sec-warn ul { margin: 0; padding-left: 18px; }
.sec-warn li { margin: 3px 0; }
.sec-warn--kids { background: var(--cream); border-color: #e8bcbc; color: var(--muted); }
.sec-warn--pay { background: var(--cream); border-color: #bcd9c5; color: var(--muted); }
.sec-note { font-size: 12.5px; color: var(--muted); line-height: 1.6; margin: 0 0 14px; }

/* The progress overlay. Steps are the server's real order; the last one only completes when
   the call actually returns, so nothing here claims success that hasn't happened. */
.secdel { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center;
  justify-content: center; padding: 20px; background: rgba(18, 26, 38, .62); }
.secdel__card { background: var(--bg); border-radius: 18px; max-width: 460px; width: 100%;
  padding: 26px; box-shadow: 0 24px 60px rgba(0, 0, 0, .28); }
.secdel__card h3 { margin: 0 0 6px; font-family: var(--head); font-size: 20px; color: var(--ink); }
.secdel__sub { margin: 0 0 18px; font-size: 13.5px; color: var(--muted); line-height: 1.55; }
.secdel__steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; }
.secdel__steps li { display: flex; align-items: center; gap: 11px; font-size: 13.5px;
  color: #9aa2ac; transition: color .2s; }
.secdel__dot { width: 16px; height: 16px; border-radius: 50%; flex: 0 0 auto;
  border: 2px solid #d7dce2; position: relative; }
.secdel__steps li.is-doing { color: var(--ink); font-weight: 600; }
.secdel__steps li.is-doing .secdel__dot { border-color: var(--muted); border-top-color: transparent;
  animation: secdel-spin .7s linear infinite; }
.secdel__steps li.is-done { color: var(--muted); }
.secdel__steps li.is-done .secdel__dot { border-color: var(--muted); background: #2f6b45; }
.secdel__steps li.is-done .secdel__dot::after { content: ""; position: absolute; left: 3.5px;
  top: .5px; width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg); }
.secdel__steps li.is-failed { color: var(--muted); }
.secdel__steps li.is-failed .secdel__dot { border-color: var(--muted); background: #b23b3b; }
.secdel__err { margin: 16px 0 0; font-size: 13px; line-height: 1.55; color: var(--muted); }
@keyframes secdel-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .secdel__steps li.is-doing .secdel__dot { animation: none; } }

/* ---------- Membership perks: locked tabs and locked pages ----------
   Greyed, not hidden. A teacher should be able to see what they are not buying — a tab that is
   absent sells nothing, and an empty "upgrade to unlock" panel says nothing about whether it is
   worth $49. So the tab stays, the page renders the real thing, and only the CONTENT is inert. */
.side__link.is-locked { color: var(--muted); }
.side__link.is-locked:hover { color: var(--ink-2); }
.side__lock { margin-left: auto; display: inline-flex; opacity: .55; }
.side__lock svg { width: 14px; height: 14px; }

/* ---- Membership perk, locked ------------------------------------------------------------
 *
 * FULL WIDTH. This was `.perk { max-width: 760px }`, which put a sales page for a $49 product
 * in a column narrower than the screen it was selling on, under a lock icon. The preview is the
 * argument, so the preview gets the room.
 *
 * The greying stays a filter on the PREVIEW ONLY. Dimming the whole page would grey the price
 * and the button too — the two things the page exists to make legible.
 */
.perk { max-width: none; }

.perk-hero {
  position: relative; overflow: hidden;
  border-radius: 20px; padding: 40px 40px 36px;
  background:
    radial-gradient(120% 140% at 100% 0%, var(--purple-tint) 0%, transparent 55%),
    radial-gradient(110% 130% at 0% 100%, var(--green-tint) 0%, transparent 60%),
    var(--cream);
}
.perk-hero__text { position: relative; max-width: 62ch; }
.perk-hero__chip {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
  color: var(--purple-ink); background: var(--glass);
  border-radius: 999px; padding: 6px 13px; margin-bottom: 16px;
}
.perk-hero__chip svg { display: block; }
.perk-hero h1 { font-size: clamp(30px, 4.2vw, 46px); line-height: 1.08; margin: 0 0 12px; letter-spacing: -.02em; }
.perk-hero p { margin: 0; font-size: clamp(15px, 1.4vw, 18px); line-height: 1.55; color: var(--ink-2); }
.perk-hero__cta { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-top: 24px; }
.perk-hero__from { font-size: 13.5px; color: var(--muted); }
.perk-hero__now { margin-top: 12px; font-size: 13.5px; color: var(--purple-ink); font-weight: 600; }

.perk-stage { margin-top: 28px; }
.perk-stage__label {
  font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted-2); margin-bottom: 10px;
}
.perk-preview {
  position: relative; border-radius: 18px; padding: 28px;
  background: var(--bg); box-shadow: 0 1px 0 var(--line), 0 18px 40px -28px rgba(15, 23, 42, .35);
  /* Unmistakably not-yours, and still readable enough to judge. Fully greyed reads as broken;
     un-greyed reads as available. */
  filter: grayscale(1) opacity(.62);
  user-select: none;
}
/* The fake UI inside the preview. Restored from the pre-redesign rules with the sizes folded in
 * rather than layered as overrides — it scales up with the page instead of staying a 760px card
 * marooned in the middle of it. */
.perk-fake { display: flex; flex-direction: column; gap: 14px; max-width: 900px; margin: 0 auto; }
.perk-fake__l { font-size: 13px; font-weight: 700; color: var(--muted); }
.perk-fake__in, .perk-fake__out, .perk-fake__card {
  border-radius: 14px; background: var(--cream); padding: 16px 18px; font-size: 15px;
}
.perk-fake__btn {
  align-self: flex-start; border-radius: 999px; background: var(--ink-fill); color: #fff;
  padding: 11px 22px; font-weight: 700; font-size: 14.5px;
}
.perk-fake__row { display: flex; justify-content: space-between; font-size: 13.5px; color: var(--muted); }
.perk-fake__note { font-size: 13px; color: var(--muted); }
.perk-fake__out { display: grid; gap: 4px; }
.perk-fake__out b { font-size: 15px; }
.perk-fake__out span { font-size: 14px; margin-bottom: 8px; }
.perk-fake__card { display: grid; gap: 6px; box-shadow: 0 0 0 1px var(--line-2); }
.perk-fake__card.is-behind { opacity: .5; transform: scale(.97); }
.perk-fake__acts { display: flex; gap: 10px; margin-top: 6px; }
.perk-fake__acts span { border-radius: 999px; padding: 8px 18px; background: var(--line); font-weight: 700; font-size: 13.5px; }
.perk-fake__acts .is-yes { background: var(--green); color: #fff; }

.perk-pkgs {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-top: 28px;
}
/* FLEX, NOT GRID, and the CTA takes the slack. These cards hold pitches of very different
   lengths — Seed's is two lines, Sprout's runs to five — and with `align-content: start` each
   button landed wherever its own text happened to stop. Four call-to-actions at four different
   heights read as four different components rather than one choice made four ways.
   `margin-top: auto` on the CTA needs free space to consume, which packed grid rows never
   leave. */
.perk-pkg {
  position: relative; border-radius: 16px; padding: 20px 18px 18px;
  background: var(--bg); box-shadow: 0 0 0 1px var(--line-2);
  display: flex; flex-direction: column; gap: 4px; text-align: left;
}
.perk-pkg__n { font-size: 16px; }
.perk-pkg__p { font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
.perk-pkg__p small { font-size: 13px; font-weight: 600; color: var(--muted); letter-spacing: 0; }
.perk-pkg__w { font-size: 13.5px; color: var(--muted); }

/* The membership pitch, brought onto this page so nobody has to leave a perk to read it. */
.perk-pkg__keep { display: grid; gap: 1px; margin-top: 8px; padding: 10px 12px; border-radius: 10px; background: var(--green-tint); }
.perk-pkg__keep b { font-size: 14.5px; color: var(--green-ink); }
/* THE QUEUED RATE, on the current package only and only while one is queued. Muted rather than
   green: it is not what the teacher is earning yet, and matching the figure above it would read as
   two live rates. See pendingLine() in packages.js for why this line exists at all. */
.perk-pkg__pending { margin-top: 3px; font-size: 11.5px; font-weight: 600; color: var(--muted); }
.perk-pkg__keep span { font-size: 12.5px; color: var(--muted); }
.perk-pkg__tagline { margin-top: 8px; font-size: 13px; font-weight: 600; }
.perk-pkg__hook { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.perk-pkg__val { margin-top: 2px; font-size: 12.5px; font-weight: 700; }
/* What THIS perk costs on THIS package — the reason they are here, so it is set apart. */
.perk-pkg__this {
  margin-top: 8px; padding-top: 8px; font-size: 12.5px; color: var(--muted);
  box-shadow: inset 0 1px 0 var(--line-2); display: grid; gap: 1px;
}
.perk-pkg__this i { font-style: normal; font-weight: 700; color: var(--ink); }

/* var(--black), matching .btn--green in styles.css — which is a DARK button despite the name.
   This first used var(--green) with white text, which is #a3c893: about 1.9:1 against white and
   unreadable. Every other primary action on the site is the dark one, so this is both the legible
   choice and the consistent one. */
/* ONE SHAPE FOR ALL FOUR STATES. Buyable, current and free differ only in colour and weight,
   never in size or position: same height, same radius, same width, all pinned to the bottom of
   the card by `margin-top: auto`. A row of buttons where one is a pill, one is bare text and
   one sits forty pixels higher looks like a bug even when every individual state is correct. */
.perk-pkg__cta {
  margin-top: auto; text-align: center;
  display: flex; align-items: center; justify-content: center;
  min-height: 42px; border-radius: 10px; padding: 10px 12px;
  font-size: 13.5px; font-weight: 700; background: var(--black); color: #fff;
}
button.perk-pkg:hover .perk-pkg__cta { background: #2a2b33; }
/* The non-actions keep the footprint and drop the emphasis — they are stating a fact, not
   offering one, so they must not look pressable while still holding the same line. */
.perk-pkg__cta.is-off { background: var(--line); color: var(--muted); font-weight: 600; }
.perk-pkg__cta.is-mine { background: var(--green-tint); color: var(--green-ink); }

/* A card that buys something is a real button: it has to look pressable, and it has to BE
   focusable. Everything else stays a div so nothing offers what it cannot do. */
button.perk-pkg {
  font: inherit; color: inherit; text-align: left; cursor: pointer; width: 100%;
  border: 0; transition: box-shadow .15s ease, transform .15s ease;
}
button.perk-pkg:hover { transform: translateY(-2px); box-shadow: 0 0 0 2px var(--green), 0 10px 24px rgba(0,0,0,.09); }
button.perk-pkg:focus-visible { outline: 3px solid var(--purple-ink); outline-offset: 2px; }
button.perk-pkg:active { transform: translateY(0); }
.perk-pkg__tag {
  position: absolute; top: -9px; left: 18px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  border-radius: 999px; padding: 3px 10px;
  color: var(--green-ink); background: var(--green-tint); box-shadow: 0 0 0 1px var(--bg);
}
.perk-pkg__tag--u { color: var(--purple-ink); background: var(--purple-light); }
/* The one they are ON and the one that would UNLOCK it are marked differently on purpose —
   they are different facts, and on Seed they are never the same card. */
.perk-pkg.is-mine { box-shadow: 0 0 0 2px var(--green); }
.perk-pkg.is-unlocks { box-shadow: 0 0 0 2px var(--purple-ink); }
/* Locked by the one-change-a-month rule. DELIBERATELY NOT FADED — the card still has to be read.
   A teacher who cannot switch until the 1st is exactly the person comparing packages for the 1st,
   and dimming the prices and perks they are deciding between would take the page away from its
   only remaining use. The greyed CTA already says it is not available; the content should not
   have to say it twice. */
.perk-pkg.is-locked { border-style: dashed; }

@media (max-width: 1000px) { .perk-pkgs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) {
  .perk-hero { padding: 28px 22px 26px; border-radius: 16px; }
  .perk-preview { padding: 18px; }
  .perk-pkgs { grid-template-columns: 1fr; }
}

/* Pod Builder credits Claude once, quietly, at the foot of the page - not in the heading,
   because it is a tool the product uses rather than a feature the product sells. */
.perk-credit {
  margin: 26px 4px 0;
  font-size: 12.5px;
  color: var(--muted);
}


/* ---- Buying a package without leaving the page (js/dashboard/membership-buy.js) ---------- */
.mbuy {
  position: fixed; inset: 0; z-index: 200; display: grid; place-items: center; padding: 24px;
  background: rgba(16, 22, 18, .55); backdrop-filter: blur(3px);
}
body.mbuy-open { overflow: hidden; }
.mbuy__box {
  position: relative; width: min(560px, 100%); max-height: min(88vh, 900px);
  display: flex; flex-direction: column;
  background: var(--bg); border-radius: 18px; box-shadow: 0 24px 64px rgba(0,0,0,.32);
}
.mbuy__head { padding: 20px 22px 12px; display: grid; gap: 2px; }
.mbuy__title { font-size: 17px; }
.mbuy__sub { font-size: 13px; color: var(--muted); }
.mbuy__x {
  position: absolute; top: 12px; right: 12px; width: 34px; height: 34px; border: 0;
  border-radius: 50%; background: var(--line); color: var(--ink); font-size: 20px; line-height: 1;
  cursor: pointer;
}
.mbuy__x:hover { background: var(--line-2); }
/* Stripe's iframe sizes itself, so the scroll has to live here or a long form is simply cut off
   on a laptop screen. */
.mbuy__body { overflow: auto; padding: 0 14px 16px; }
.mbuy__spin { padding: 28px 8px 34px; text-align: center; color: var(--muted); font-size: 14px; }
.mbuy__err { padding: 8px 8px 20px; color: var(--danger, var(--muted)); font-size: 14px; line-height: 1.5; }

@media (max-width: 640px) {
  .mbuy { padding: 0; place-items: end stretch; }
  .mbuy__box { width: 100%; max-height: 94vh; border-radius: 18px 18px 0 0; }
}

/* ---- Pod Builder ------------------------------------------------------------------------
   Minimal on purpose: the real editor (docs/course-builder.md, Session 1) replaces this page, so
   these styles exist to make a draft READABLE, not to be a design anyone has to keep. */
/* THE PICKER ROW WAS UPSIDE DOWN. `flex: 1 1 260px` let the dropdown grow to fill the row — 890px
   at desktop width — while "Draft the course" sat beside it at 111px. The control that matters
   least dominated the page and the action the teacher came for was the smallest thing on it.
   Measured, not guessed: rendered the real module and read the bounding boxes.

   The select is capped now and the button leads. A pod name is short; nothing about choosing one
   needs half a screen. */
.cb-pick { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 18px; }
.cb-pick label { font-weight: 600; font-size: 14px; }
.cb-pick select { flex: 0 1 clamp(220px, 32vw, 420px); padding: 11px 14px;
                  border: 1px solid var(--line-2, #efeeea); border-radius: 10px; font: inherit;
                  background: var(--bg); }
.cb-pick .btn { flex: none; padding-inline: 22px; }

/* WHAT YOU GET AND WHAT IT COSTS, not fine print. This line carries "one document per session"
   and how many drafts remain on the package — the two facts a teacher needs before spending one
   of a small monthly allowance — and it was set at 12.5px muted grey, the same treatment as a
   disclaimer. It reads at body size now, on its own line, with the remaining count able to stand
   out from the description. */
.cb-note { flex-basis: 100%; margin: 10px 0 0; font-size: 13.5px; line-height: 1.55;
           color: var(--ink-2, var(--muted)); max-width: 68ch; }
.cb-note b { color: var(--ink, var(--ink)); font-weight: 650; }
.cb-partial { margin: 0 0 14px; padding: 12px 14px; border-radius: 10px;
              background: var(--bg); color: var(--muted); font-size: 13.5px; }
.cb-partial p { margin: 0; }
.cb-partial__act { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
/* The way OUT of a partial draft, so it reads as the next step rather than as more warning text. */
.cb-partial__go { border: 0; border-radius: 9px; padding: 8px 16px; cursor: pointer;
  font: 700 12.5px var(--sans); color: #fff;
  background: linear-gradient(120deg, #7C3AED, #A855F7 55%, #22C55E);
  background-size: 180% 100%; transition: background-position .35s ease; }
.cb-partial__go:hover:not(:disabled) { background-position: 100% 0; }
.cb-partial__go:disabled { background: #c9b98a; cursor: default; }
/* Said next to the button, not in the paragraph above it. The question a teacher has at the moment
   of pressing is "what does this cost me", and it is answered where they are looking. */
.cb-partial__n { font-size: 12px; color: var(--muted); }
.cb-partial__err { margin-top: 8px !important; font-size: 12.5px; color: var(--muted); }
.cb-partial [hidden] { display: none !important; }
.cb-doc { margin: 0 0 16px; padding: 16px; border-radius: 12px; background: var(--bg); }
.cb-doc h3 { margin: 0 0 8px; font-size: 16px; }
/* The body is model-written and edited by a teacher, so it has to read like a document rather
   than like app chrome — generous line height, real list indents. */
.cb-body { font-size: 14.5px; line-height: 1.6; color: var(--ink-2, var(--ink)); }
.cb-body h2 { font-size: 15.5px; margin: 14px 0 6px; }
.cb-body h3 { font-size: 14.5px; margin: 12px 0 4px; }
.cb-body ul, .cb-body ol { margin: 6px 0 6px 20px; }
.cb-body li { margin: 3px 0; }
.cb-credit { margin: 18px 0 0; font-size: 11.5px; color: var(--muted-2, #94a3b8); }

/* Orders — membership payments and receipts.
   No borders anywhere: separation is fill and shadow, same as the rest of the app. */
/* The cancel line on Packages. Same quiet treatment as the one on Orders — cancelling is a
   legitimate thing a paying customer may want to do, and dressing it as a danger is how a product
   makes leaving feel like a transgression. */
/* The once-a-month rule, said once, above the cards. Informational rather than alarming — the
   teacher has not done anything wrong, they have simply already used this month's change. */
.pkgs-lock { display: flex; align-items: flex-start; gap: 9px; margin: 0 0 18px;
  background: var(--cream); border-radius: 12px; padding: 12px 14px;
  font-size: 13.5px; line-height: 1.55; color: var(--ink); }
.pkgs-lock svg { flex: none; margin-top: 1px; color: var(--muted); }
.pkgs-lock b { font-weight: 700; }

.pkgs-cancel { margin: 18px 0 0; font-size: 14px; color: var(--muted); text-align: center; }
.pkgs-cancel.is-ending { color: var(--muted); }
.pkgs-cancel__go { background: none; border: 0; padding: 6px 4px; cursor: pointer; font: inherit;
  font-size: 14px; color: var(--muted); text-decoration: underline; text-underline-offset: 3px;
  border-radius: 8px; }
.pkgs-cancel__go:hover { color: var(--ink); }
.pkgs-cancel__go:focus-visible { outline: 2px solid #5F8149; outline-offset: 2px; }

.ord__lede { margin: 0 0 20px; color: var(--muted); font-size: 14px; }
/* The list needs a name now that the card above it no longer announces itself with a shadow. */
.ord__label { margin: 0 0 10px; font-size: 13px; font-weight: 700; color: var(--muted);
  letter-spacing: .02em; }
.ord__list { display: flex; flex-direction: column; gap: 10px; }
.ord__row {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 16px 18px; border-radius: 14px; background: var(--bg);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .04);
}
.ord__main { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.ord__tier { font-size: 15px; }
.ord__period { font-size: 13px; color: var(--muted); }
.ord__right { display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
.ord__amt { font-weight: 700; font-variant-numeric: tabular-nums; }
.ord__date { font-size: 13px; color: var(--muted); white-space: nowrap; }
.ord__dl {
  font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap;
  color: var(--purple-ink); padding: 7px 12px; border-radius: 9px; background: var(--lilac, var(--cream));
}
.ord__dl:hover { filter: brightness(.97); }
.ord__dl.is-off { color: var(--muted); background: transparent; }
.ord__foot { margin-top: 14px; font-size: 13px; color: var(--muted); }
.ord__empty {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
  padding: 34px; border-radius: 16px; background: var(--bg);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .06);
}
.ord__empty b { font-size: 16px; }
.ord__empty span { color: var(--muted); font-size: 14px; }
.ord__empty .btn { margin-top: 8px; }
@media (max-width: 640px) {
  .ord__row { flex-direction: column; align-items: flex-start; gap: 10px; }
  .ord__right { width: 100%; justify-content: space-between; }
}


/* ---- The Packages view (js/dashboard/packages.js) ---------------------------------------- */
.pkgs-head { padding: 4px 2px 22px; max-width: 720px; }
.pkgs-head h1 { font-size: 34px; letter-spacing: -.02em; margin: 0 0 8px; }
.pkgs-head p { color: var(--muted); font-size: 15px; line-height: 1.55; margin: 0; }
/* The cards sit directly under the heading here, unlike the perk pages where a preview
   separates them, so the shared block's top margin would double the gap. */
.pkgs .perk-pkgs { margin-top: 0; }

.pkgs-note {
  margin-top: 26px; padding: 16px 18px; border-radius: 14px; background: var(--green-tint);
  display: grid; gap: 6px;
}
/* DIRECT CHILD. As `.pkgs-note b` this also caught the inline <b>Seed</b> in the paragraph
   below and made it display:flex — a bold word on a line of its own, mid-sentence. */
.pkgs-note > b { display: flex; align-items: center; gap: 8px; font-size: 14.5px; color: var(--green-ink); }
.pkgs-note p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--ink); }

/* ROOM UNDER THE LAST ROW FOR THE TWO FIXED BUTTONS — Victor, 2026-08-28: "need more space at the
   bottom. it's being blocked off."

   `.report-fab` is `position: fixed; bottom: 20px` at z-900 with the notification bell beside it,
   so together they occupy roughly the bottom 64px of the VIEWPORT at every scroll position. The
   page could be scrolled to its end and the last Buy now button would still be underneath them —
   unreachable, and looking like the table had been cut off.

   Padding on the page rather than a margin on the table, because the table is not always last and
   the obstruction belongs to the viewport, not to any one section. */
.pkgs { padding-bottom: clamp(96px, 12vh, 140px); }
.pkgs-table { margin-top: 30px; }
.pkgs-table h2 { font-size: 20px; letter-spacing: -.01em; margin: 0 0 4px; }
.pkgs-lead { margin: 0 0 12px; color: var(--muted); font-size: 13.5px; }
/* A four-package table does not fit a narrow dashboard column, and the PAGE must not scroll
   sideways to accommodate it — the table does, on its own. */
.pkgs-scroll { overflow-x: auto; border-radius: 14px; box-shadow: 0 0 0 1px var(--line-2); }

/* THE BUY COLUMN on the a-la-carte table. Narrow so the three description cells keep the width —
   the copy is what a teacher reads to decide, the button is what they press once they have.

   A FLEX ROW RATHER THAN TWO INLINE ELEMENTS AND A margin-right. Inline-block plus right-alignment
   left the count floating loose in the whitespace beside the button, at a distance that changed
   with the count's own width — "2 bought" and "12 bought" sat differently, and a row with no count
   at all had nothing holding the column together. Flex pins the gap at one value and centres the
   two against each other, so the column reads as one thing on every row whether or not it has a
   count. `justify-content: flex-end` keeps the button flush right, which is the fixed point a
   teacher's eye follows down the table. */
.pkgs-buy__cell { width: 1%; white-space: nowrap; }
.pkgs-buy__cell > * { vertical-align: middle; }
.pkgs-buy__inner { display: flex; align-items: center; justify-content: flex-end; gap: 12px; }

/* OWNED, AS A CHIP RATHER THAN AS LOOSE TEXT. Bare green words next to a button read as a caption
   on the button — as though it described what pressing it would do. A tinted pill reads as a state
   the row is IN, which is what it is: credits already purchased and not yet spent. Green because
   this is the same "you have it" the price column uses, not because it is a success message. */
.pkgs-have {
  display: inline-flex; align-items: center; font: 700 12px/1 var(--sans);
  letter-spacing: .01em; padding: 6px 10px; border-radius: 999px;
  background: var(--green-tint); color: var(--green-ink); }

/* THE BUTTON IS THE ACTION ON THIS TABLE and did not look like one — it inherited bare `.btn`, so
   it rendered as pale grey furniture next to three columns of text. It is a PURCHASE, and the one
   thing on the row a teacher can do.

   Soft purple rather than solid: there are four of these stacked, and four solid CTAs in a column
   compete with each other and with the package cards above, which are the primary decision. Soft
   is the product's established "actionable, not urgent" and it is what every other secondary
   action here already uses. */
.pkgs-buy {
  background: var(--purple-tint); color: var(--purple-ink);
  font-weight: 700; padding: 9px 16px; border-radius: 10px; box-shadow: none; }
.pkgs-buy:hover:not([disabled]) { background: #e8e0f7; }
.pkgs-buy:active:not([disabled]) { transform: translateY(1px); }
.pkgs-buy[disabled] { opacity: .6; cursor: default; }
.pkgs-table table { width: 100%; border-collapse: collapse; background: var(--bg); min-width: 620px; }
.pkgs-table th, .pkgs-table td { padding: 12px 14px; text-align: left; font-size: 13.5px; }
.pkgs-table thead th { font-size: 12.5px; font-weight: 800; letter-spacing: .02em; }
.pkgs-table tbody tr + tr th, .pkgs-table tbody tr + tr td { box-shadow: inset 0 1px 0 var(--line-2); }
.pkgs-table tbody th { font-weight: 600; }
.pkgs-table .is-included { color: var(--green-ink); font-weight: 700; }
.pkgs-table .is-payuse { color: var(--muted); }
.pkgs-table .is-none { color: var(--muted); opacity: .7; }

@media (max-width: 640px) {
  .pkgs-head h1 { font-size: 26px; }
}

/* Break-even, on every card — the number a monthly fee is meaningless without. */
.perk-pkg__be {
  margin-top: 8px; padding-top: 8px; font-size: 12px; line-height: 1.45; color: var(--muted);
  box-shadow: inset 0 1px 0 var(--line-2);
}
.perk-pkg__be b { color: var(--ink); }

/* ---- Take-home calculator ---------------------------------------------------------------- */
.pkgs-calc { margin-top: 30px; }
.pkgs-calc h2 { font-size: 20px; letter-spacing: -.01em; margin: 0 0 4px; }
.pkgs-calc__in { display: grid; gap: 6px; font-size: 13px; font-weight: 600; max-width: 260px; margin-bottom: 14px; }
.pkgs-calc__in span { display: flex; align-items: center; gap: 6px; background: var(--bg);
  border-radius: 10px; padding: 0 12px; box-shadow: 0 0 0 1px var(--line-2); }
.pkgs-calc__in i { font-style: normal; color: var(--muted); }
.pkgs-calc__in input { flex: 1; border: 0; background: none; padding: 11px 0; font: inherit; font-size: 15px; width: 100%; }
.pkgs-calc__in input:focus { outline: none; }
.pkgs-calc__in:focus-within span { box-shadow: 0 0 0 2px var(--green-ink); }

.pkgs-calc__rows { display: grid; gap: 8px; }
.pkgs-calc__row {
  display: grid; grid-template-columns: 90px 1fr auto auto; gap: 6px 14px; align-items: center;
  background: var(--bg); border-radius: 12px; padding: 12px 16px; box-shadow: 0 0 0 1px var(--line-2);
}
.pkgs-calc__row.is-best { box-shadow: 0 0 0 2px var(--green-ink); }
.pkgs-calc__sum { font-size: 12.5px; color: var(--muted); }
.pkgs-calc__take { font-size: 18px; font-weight: 800; letter-spacing: -.01em; }
.pkgs-calc__vs { font-size: 12.5px; color: var(--muted); min-width: 108px; text-align: right; }
.pkgs-calc__best {
  grid-column: 1 / -1; font-size: 11px; font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase; color: var(--green-ink);
}
.pkgs-calc__note { margin: 12px 0 0; font-size: 13px; line-height: 1.6; color: var(--muted); }

/* ---- Plain-language terms ---------------------------------------------------------------- */
.pkgs-terms { margin-top: 30px; }
.pkgs-terms h2 { font-size: 20px; letter-spacing: -.01em; margin: 0 0 10px; }
.pkgs-terms ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.pkgs-terms li {
  background: var(--bg); border-radius: 12px; padding: 12px 16px;
  box-shadow: 0 0 0 1px var(--line-2); font-size: 13.5px; line-height: 1.55; color: var(--muted);
}
.pkgs-terms li b { color: var(--ink); }

@media (max-width: 720px) {
  .pkgs-calc__row { grid-template-columns: 1fr auto; }
  .pkgs-calc__sum { grid-column: 1 / -1; }
  .pkgs-calc__vs { text-align: left; min-width: 0; }
}


/* ---- Course Spotlight (js/dashboard/spotlight.js) ---------------------------------------- */
/* There was NO css for any of this. Every .spot-* class rendered as bare HTML: the label and the
   select on one line, "Runs for 7 daysEnds Fri, Aug 28" run together, and the thing a teacher
   just paid for looking like a form from 1998. */

.spot { position: relative; display: grid; gap: 16px; }
.spot-confetti { position: absolute; inset: 0 0 auto 0; pointer-events: none; z-index: 5; }

.spot-allow { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--muted); }
.spot-allow b { color: var(--ink); }
/* BOUGHT SPOTLIGHTS, beside the monthly pips but visibly not one of them. They are a different
   thing — they do not expire on the 1st and they are not drawn as tokens — so a teacher reading
   "1 of 3 left + 2 bought" can see at a glance which half resets and which half they own. This is
   the same word the Packages chip uses; two screens calling two different numbers "left" is what
   made a bought spotlight look like it had vanished. */
.spot-allow__buy {
  padding: 2px 8px; border-radius: 999px;
  background: var(--green-tint); color: var(--green-ink); font-size: 13px; white-space: nowrap;
}
.spot-allow__buy b { color: inherit; }
/* SPENT TOKENS STAY VISIBLE. This ghosted every pip to 35% once the allowance ran out, so the
   row a teacher most needs to understand — none left — rendered as a faint smudge beside the
   text and read as a rendering fault rather than as an empty slot. An empty token should look
   empty, not half-erased. */
/* Tokens, not a statistic — a small number of spends you hold and use up. */
.spot-pips { display: inline-flex; gap: 5px; }
.spot-pips i {
  width: 10px; height: 10px; border-radius: 50%; background: var(--line-2);
  box-shadow: inset 0 0 0 1px rgba(20,21,26,.14);
}
.spot-pips i.is-on { background: var(--green-fill); box-shadow: 0 0 0 3px var(--green-tint); }

/* The picker is the OTHER half of the same object, not a form parked underneath it. A warm rule
   along the top ties it to the hero, and the whole card lifts on hover the way something you are
   about to spend on should. */
.spot-card {
  position: relative; overflow: hidden;
  display: grid; gap: 12px; align-items: start;
  background: var(--bg); border-radius: 18px; padding: 24px 26px;
  box-shadow: 0 0 0 1px var(--line-2), 0 8px 22px rgba(20,21,26,.05);
}
.spot-l {
  font-size: 10.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted);
}
/* A real control rather than the browser's default, which was the single most unfinished thing
   on the screen — a system dropdown sitting in the middle of a designed card. */
.spot-form select {
  width: 100%; appearance: none; -webkit-appearance: none;
  font: inherit; font-size: 15px; font-weight: 600; color: var(--ink);
  padding: 13px 40px 13px 15px; border: 0; border-radius: 12px;
  background-color: var(--field-fill, var(--cream));
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 15px center;
  box-shadow: inset 0 0 0 1px var(--line-2); cursor: pointer;
}
.spot-form select:focus { outline: none; box-shadow: inset 0 0 0 2px var(--green-ink); }
.spot-row {
  display: flex; flex-wrap: wrap; gap: 6px 10px; font-size: 12.5px; color: var(--muted);
}
.spot-row span {
  background: var(--line); border-radius: 999px; padding: 5px 12px;
}
.spot-row b { color: var(--ink); font-weight: 700; }
/* The promise. It is the reason to press the button, so it is the biggest line in the card. */
.spot-above {
  margin: 4px 0 2px; font-size: 15px; line-height: 1.45; color: var(--ink); font-weight: 600;
}
.spot-above b {
  font-size: 19px; font-weight: 800; color: #b8720f;
}
.spot-card .btn { justify-self: start; font-size: 15px; padding: 13px 26px; }
.spot-note { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--muted); }
.spot-note--quiet { opacity: .8; }
.spot-err { margin: 0; font-size: 13.5px; color: var(--danger, var(--muted)); }

/* ---- The live card: this is the thing they bought, so it looks like it ------------------- */
/* RESTRAINT, NOT A WASH. Every card was a full-bleed gold gradient with an animated glow behind
   it, and three of them filled a viewport — so the loudest surface on the dashboard was also the
   most repeated, and the pod's own name had to compete with its background. The placement is the
   thing worth celebrating; the card is not.

   The warmth is still here, spent where it carries meaning: the live dot, the progress fill, and
   a hairline of colour at the top edge. Everything else is the ordinary card surface, which is
   what lets the title read as the loudest thing on the card rather than the second loudest. */
/* LIVE SPOTLIGHTS PACK INTO A GRID. Each card was a full-width slab, so four concurrent runs
   were four screens of white holding forty words. Once a run is live there is nothing on it to
   act on — it is a readout — so it earns a tile, two or three across, and the whole set is
   visible without scrolling. */
.spot-live { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }

.spotx {
  position: relative; overflow: hidden; border-radius: 14px; color: var(--ink);
  background: var(--bg);
  padding: 14px 16px 13px;
  box-shadow: 0 1px 2px rgba(20,21,26,.06), 0 8px 24px rgba(20,21,26,.06);
  /* Its own stacking context, so the banner can sit at z-index:-1 - above the card's white
     (negative-z children paint over the context root's background) yet below every child,
     with nothing else needing a z-index. Without isolation, -1 drops the image behind the
     white card and it simply vanishes. */
  isolation: isolate;
}
/* The pod's cover banner as a wash. 15% is enough to make each tile recognisably ITS pod
   without the text ever fighting a photograph - the numbers on this card are the product. */
.spotx__bg {
  position: absolute; inset: 0; z-index: -1;
  width: 100%; height: 100%; object-fit: cover;
  opacity: .15; pointer-events: none;
}
/* THE SPOTLIGHT EDGE, DEFINED ONCE FOR EVERY CARD THAT WEARS IT.
   A 2px edge, and deliberately the only structural gold. It exists so a group of live placements
   reads as a group from across the page — an ordinary dashboard card has no such edge — and it is
   thin enough that the pod's name still wins the card. At 3px, stacked three deep, it started to
   read as three gold bars with cards attached.

   BOTH SELECTORS SHARE ONE RULE because they were already drifting: the live card was thinned to
   2px with a two-stop gradient while the picker beneath it kept 4px and a three-stop one, so on a
   page showing both they read as two different design languages a few hundred pixels apart. Two
   copies of a value that must match is the state that produced the mismatch; one copy cannot. */
.spotx::before,
.spot-card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, #f0a92c, #e8892b);
}

.spotx__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
/* A dot and a word. The badge was an uppercase black pill with a star, heavier than the pod name
   underneath it — a status marker outweighing the thing whose status it reports. */
.spotx__live {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; color: var(--muted);
}
.spotx__live i {
  width: 6px; height: 6px; border-radius: 50%; background: #e8892b;
  box-shadow: 0 0 0 3px rgba(232,137,43,.18);
}
/* The countdown holds the head's right corner — the card's one moving part, at label size.
   It was 17px on its own line; on a tile that made the clock outweigh the pod. */
.spotx__time {
  font-size: 12.5px; font-weight: 650; color: var(--ink);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum";
}

.spotx__title {
  margin: 8px 0 0; font-size: 15.5px; letter-spacing: -.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.spotx__where {
  margin: 2px 0 0; font-size: 12.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.spotx__bar {
  height: 4px; border-radius: 999px; background: rgba(20,21,26,.07);
  overflow: hidden; margin-top: 12px;
}
.spotx__bar i {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, #f0a92c, #e8892b); transition: width 1s linear;
}

/* One muted line of whatever is true so far: "7 seen · 2 opened · above 6". */
.spotx__facts { margin: 9px 0 0; font-size: 12.5px; color: var(--muted); }
.spotx__facts b { color: var(--ink); font-weight: 650; font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .spotx__bar i { transition: none; }
}

/* The picker sits beside a picture of what it buys — a form on a field of white asked teachers
   to spend a monthly slot without ever showing the result. */
.spot-grid { display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 28px; align-items: start; }
.spot-form { display: grid; gap: 10px; align-content: start; }
.spot-form select { max-width: 100%; }

.spot-prev { display: grid; gap: 8px; min-width: 0; }
.spot-prev__label {
  font-size: 10.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted);
}
.spot-prev__wait {
  /* Was a single centred line, which is all it ever held. It now carries a heading, a reason and
     sometimes a way out, so it has to stack them — inline <b> and <span> ran together into one
     sentence with no break. */
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 28px 12px; text-align: center; color: var(--muted); font-size: 13px;
}
.spot-prev__wait b { font-size: 15px; color: var(--ink); }
.spot-prev__wait span { max-width: 44ch; line-height: 1.5; }
.spot-prev__wait .btn { margin-top: 4px; }

/* THE REAL MARKETPLACE CARD, AS A THUMBNAIL. cardHTML at full fidelity, scaled to .55 and
   CROPPED by the stage. The first transform attempt (.78, no crop) failed because a scaled
   element keeps its ORIGINAL layout box — 163px of dead space under the card, and a negative
   percentage margin that resolves against WIDTH could never reclaim a height. The stage's
   overflow:hidden is what fixes that this time: the leftover layout height falls outside the
   clip instead of holding the page open. The scaler is width: 100%/.55 so the card lays out at
   the width it believes it has, then shrinks into the column.

   pointer-events off: it is a picture of a listing, not a link to one. */
.spot-prev__stage.is-mini {
  position: relative; max-height: 300px; overflow: hidden; border-radius: 14px;
}
/* The crop reads as intentional, not accidental: the last visible pixels dissolve into the
   card's own white rather than slicing a neighbour mid-line. */
.spot-prev__stage.is-mini::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 44px; pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--bg));
}
.spot-prev__scale { transform: scale(.55); transform-origin: top left; width: calc(100% / .55); }
.spot-prev__stage .pod { pointer-events: none; }
/* The one being sold sits proud; the neighbours it goes above recede. */
.spot-prev__lit .pod { box-shadow: 0 0 0 2px var(--green-ink), 0 14px 30px rgba(20,21,26,.14); }
.spot-prev__rest { opacity: .5; filter: saturate(.7); }
.spot-prev__rest .pod { margin-top: 10px; }
.spot-prev__foot { margin: 2px 0 0; font-size: 11px; color: var(--muted); }

@media (max-width: 860px) {
  .spot-grid { grid-template-columns: 1fr; gap: 20px; }
}

/* ---- Matchmaking (match-browse.js, match-requests.js) --------------------

/* The feed's time window. A segmented pair rather than a dropdown: there are two states, both
   worth naming, and a <select> would hide the one you are not in behind a click. */
.mm-win { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 14px; }
.mm-win__tabs { display: inline-flex; background: var(--line-2); border-radius: 999px; padding: 3px; }
.mm-win__b {
  border: 0; background: transparent; cursor: pointer; font: inherit; font-size: 12.5px;
  font-weight: 600; color: var(--muted); padding: 5px 12px; border-radius: 999px;
}
.mm-win__b.is-on { background: var(--bg); color: var(--ink); box-shadow: 0 1px 3px rgba(16,20,28,.14); }
.mm-win__b:focus-visible { outline: 2px solid var(--purple-ink); outline-offset: 1px; }
/* Says what is being held back. Never rendered on "All", where there is nothing to disclose. */
.mm-win__n { font-size: 12.5px; color: var(--muted); }
.mm-empty .mm-win__all { margin-top: 10px; }
   Separation by fill and shadow, not by strokes, matching the rest of the
   dashboard. The one deliberate exception is .mm-card.is-sent, where a spent
   connection needs to read as done at a glance across a list of twenty. */
/* ------------------------------------------------------- MATCHMAKING, TEACHER SIDE ---
   A READING SURFACE, NOT A DASHBOARD. Every card is a parent describing their child to a
   stranger, and the decision a teacher makes here — spend a connection or not — can only be
   answered by reading what they wrote. So the note gets the measure and the leading, the facts
   get one quiet line, and nothing on the card competes with the words.

   Restraint is the whole design: one accent, hairline separation, and type doing the work. */
.mm-wrap { max-width: 720px; }
.mm-head { margin: 0 0 26px; }
.mm-title { font-family: var(--head); font-size: 30px; line-height: 1.15; letter-spacing: -.015em;
  margin: 0 0 8px; }
.mm-lede { color: var(--muted); font-size: 15px; line-height: 1.6; margin: 0 0 14px; max-width: 60ch; }
.mm-count { display: inline-block; font-size: 13px; font-weight: 600; color: var(--muted);
  background: var(--cream); padding: 6px 13px; border-radius: 999px; }
.mm-count.is-low { color: var(--muted); background: var(--cream); }
.mm-count.is-out { color: var(--muted); background: var(--cream); }

/* 3-4 ACROSS, AND THE COUNT IS NOT TYPED ANYWHERE. Victor asked for "3x4 cards"; auto-fill with a
   288px floor gives four across on a wide dashboard, three at laptop width, two on a tablet and
   one on a phone, without a single breakpoint naming a number. A fixed `repeat(4, 1fr)` would be
   the same layout until the sidebar or the window moved. */
.mm-list { display: grid; gap: 18px;
  grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)); }

/* THE TILE. A column, because the footer has to sit on the same line across every card in a row
   whatever the headline and the note do above it — that alignment is the whole reason a grid of
   tiles reads as one set rather than as four unrelated boxes. Grid stretches the rows, the note
   takes the slack, the footer lands level. */
.mmq { position: relative; display: flex; flex-direction: column;
  background: var(--bg); border-radius: 20px; padding: 22px 22px 18px;
  box-shadow: 0 1px 2px rgba(16,24,40,.05), 0 6px 16px rgba(16,24,40,.04);
  transition: box-shadow .24s ease, transform .24s ease; }
.mmq:hover { transform: translateY(-2px);
  box-shadow: 0 2px 4px rgba(16,24,40,.06), 0 14px 32px rgba(16,24,40,.09); }
/* Shared with a family already: calm and finished, not greyed out. They are still a live lead. */
.mmq.is-sent { background: var(--bg); box-shadow: inset 0 0 0 1px #dfe9da; }
.mmq.is-sent:hover { transform: translateY(-2px);
  box-shadow: inset 0 0 0 1px #dfe9da, 0 12px 26px rgba(16,24,40,.07); }

/* GOLD HAS TO BE VISIBLE ACROSS THE ROOM, not legible on inspection — "need the gold to pop".
   Three things carry it and none of them is a louder chip: a warm light falling from the top of
   the tile, a gold edge, and a shadow tinted gold instead of grey so the card sits above the
   others rather than beside them. The wash is a radial gradient rather than a flat fill because a
   flat cream tile just looks like a different card; light coming from above reads as the same
   card, lit. */
.mmq.is-gold {
  background:
    radial-gradient(130% 90% at 50% -25%, #fff4d2 0%, rgba(255,244,210,0) 62%),
    var(--bg);
  box-shadow: inset 0 0 0 1.5px #ecd08a,
    0 2px 6px rgba(125,95,20,.10), 0 18px 40px rgba(125,95,20,.16); }
.mmq.is-gold:hover { transform: translateY(-3px);
  box-shadow: inset 0 0 0 1.5px #e3c375,
    0 3px 8px rgba(125,95,20,.13), 0 26px 56px rgba(125,95,20,.22); }

/* The eyebrow row: the band chip, and for gold the words as well, because a colour alone is not a
   label and is invisible to anyone who cannot separate gold from grey. */
.mmq__band { display: flex; align-items: center; gap: 9px; margin: 0 0 12px; min-height: 22px; }
.mmq__best { font: 700 10.5px/1 var(--font-body, system-ui); letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); }

/* CLAMPED, so tiles stay comparable. An unclamped headline makes one tile twice the height of its
   neighbour and the grid row grows to fit the worst case, which is how a set of cards stops
   looking like a set. Three lines is enough for every real subject line we have. */
.mmq__subject { font-family: var(--head); font-size: 19px; line-height: 1.3; letter-spacing: -.015em;
  margin: 0; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* The commercial fact, set apart because it is the one number a teacher scans for. */
.mmq__budget { flex: none; font-size: 17px; font-weight: 700; white-space: nowrap;
  font-variant-numeric: tabular-nums; color: var(--ink); }
.mmq__budget small { font-size: 12px; font-weight: 600; color: #7a8580; }
.mmq__budget--none { font-size: 12.5px; font-weight: 600; color: #8b9490; }

.mmq__facts { margin: 8px 0 0; font-size: 13px; color: var(--muted); }

/* The parent's words, and the element that pins the footer down: flex:1 means it absorbs whatever
   height the row has spare. Clamped at four lines — enough to hear the family's voice, which is
   what actually sells a teacher on a request, and short enough that four tiles are comparable at a
   glance. The full note is on the profile view. max-width is gone; in a 288px column the measure
   is the column. */
.mmq__note { margin: 12px 0 0; flex: 1 1 auto; font-size: 14.5px; line-height: 1.65; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
.mmq__note--none { color: #98a29b; font-style: italic; }

/* PRICE AND ACTION, TOGETHER, AT THE BOTTOM OF EVERY TILE. The commercial fact and the thing you
   do about it belong on one line, in the same place on every card, so a teacher comparing four
   families reads four prices down a column instead of hunting for each one. A hairline above it
   rather than a gap: the footer is a different KIND of content from the words above it and the
   rule says so in 1px. */
.mmq__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(16,24,40,.08); }
.mmq.is-gold .mmq__foot { border-top-color: rgba(150,115,25,.20); }
.mmq.is-sent .mmq__foot { border-top-color: rgba(40,70,30,.12); }
.mmq__done { display: flex; align-items: center; gap: 7px; margin: 12px 0 0;
  font-size: 12.5px; font-weight: 600; color: var(--muted); }
.mmq__done svg { flex: none; }

/* THE CARD ACTION IS NOT A LANDING-PAGE CTA.
   Two faults met in this footer and both read as "ugly" rather than as bugs.
   FIRST, the two states shipped DIFFERENT BUTTONS: "Invite them to a pod" carried btn--sm and
   "Share my profile" did not, so one card showed a 10px-radius 13px control and the next showed a
   full pill a size larger. In a vertical list of near-identical cards that misalignment is the
   thing the eye lands on. Both are btn--sm now, fixed in the markup rather than here, because the
   size belongs to the button and not to the container it sits in.
   SECOND, btn--dark carries `box-shadow: 0 12px 28px rgba(106,84,181,.28)` — a deliberate hero
   glow, correct on a marketing page where one button is the point of the screen, and wrong
   repeated down a feed where every card has one. It made a row of cards look like a row of
   adverts. Dropped here and given back on HOVER at a third of the spread, so the control still
   answers the pointer without shouting while at rest. */
.mmq__foot .btn { box-shadow: none; }
.mmq__foot .btn--dark:hover { box-shadow: 0 5px 12px rgba(106,84,181,.22); }
/* The secondary action was reading as disabled against the card rather than as a choice: purple
   tint on near-white is about four points of separation. A deeper fill, not an outline — this
   product separates with fill, and an outline here would be the only one on the card. */
.mmq__foot .btn--soft { background: #e9e1fa; }
.mmq__foot .btn--soft:hover { background: #ddd1f5; }

.mm-err:empty { display: none; }

.mm-empty { background: var(--bg); border-radius: 18px; padding: 44px 32px; text-align: center;
  box-shadow: 0 1px 2px rgba(16,24,40,.05), 0 6px 16px rgba(16,24,40,.04); }
.mm-empty__mark { display: inline-grid; place-items: center; width: 42px; height: 42px;
  border-radius: 50%; background: var(--cream); color: var(--muted); margin: 0 0 14px; }
.mm-empty b { display: block; font-family: var(--head); font-size: 19px; }

/* THE "STILL EARLY" NOTICE on the teacher's matchmaking screen.
   Separation by fill and shadow, no stroke — the same rule the rest of this product follows, so a
   notice does not arrive looking like an error just because it is the only outlined thing on the
   page. The green wash marks it as ours-speaking-to-you rather than a warning; amber or a border
   would read as something being wrong, and nothing is wrong. */
.mm-early { background: var(--cream); border-radius: 18px; padding: 24px 26px; margin: 0 0 22px;
  box-shadow: 0 1px 2px rgba(16,24,40,.05), 0 6px 16px rgba(16,24,40,.04); }
.mm-early__h { font-family: var(--head); font-size: 18px; margin: 0 0 8px; color: var(--ink); }
.mm-early p { margin: 0; color: var(--muted); font-size: 14.5px; line-height: 1.6; max-width: 62ch; }
.mm-early__p { margin-top: 10px !important; font-weight: 600; }
@media (max-width: 620px) { .mm-early { padding: 20px; border-radius: 16px; } }

@media (max-width: 620px) {
  .mm-title { font-size: 25px; }
  .mmq { padding: 20px; border-radius: 16px; }
  .mmq__head { flex-direction: column; gap: 6px; }
  .mmq__note { font-size: 15px; }
  .mmq__foot { justify-content: stretch; }
  .mmq__foot .btn { flex: 1; }
}
/* A card that lifts under the pointer is a nice touch and a problem for anyone who has asked the
   system to stop moving things. */
@media (prefers-reduced-motion: reduce) {
  .mmq { transition: none; }
  .mmq:hover { transform: none; }
}
.mm-note { color: var(--muted); font-size: 14px; line-height: 1.55; margin: 6px 0 18px; }

.mm-reqs { display: grid; gap: 12px; }
.mm-req { background: var(--bg); border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 2px rgba(16,24,40,.06), 0 1px 3px rgba(16,24,40,.04); }
.mm-req.is-closed { opacity: .68; }

.mm-req__top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.mm-req__meta { font-size: 13px; color: var(--muted); }
.mm-req__notes { font-size: 14px; line-height: 1.5; margin: 8px 0 0; color: var(--muted); }
.mm-req__act { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.mm-sent { font-size: 13px; color: var(--muted); }
.mm-err { font-size: 13px; color: var(--muted); }
.mm-req__state { font-size: 12px; color: var(--muted); background: var(--wash); padding: 2px 8px; border-radius: 999px; }
.mm-req__wait { font-size: 13px; color: var(--muted); margin: 10px 0 0; }

.mm-t__list { display: grid; gap: 8px; margin-top: 12px; }
.mm-t { display: flex; align-items: center; gap: 10px; background: var(--wash); border-radius: 10px; padding: 8px 10px; }
/* Two actions now, not one. `See profile` answers "who is this" — the question a name and one line
   never could — and Message is the one they came to press, so it carries the fill. Wraps rather
   than squeezing: on a narrow dashboard the row is already a photo plus a name plus a headline. */
.mm-t__acts { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; justify-content: flex-end; }
/* The retract confirmation, in place of the button rather than over the page: it is a question
   about THIS request and it belongs on this card. */
.mm-retract__ask { margin-top: 8px; }
.mm-retract__ask > p { margin: 0 0 6px; font-size: 13px; color: var(--muted); }
.mm-retract__row { display: flex; gap: 6px; flex-wrap: wrap; }
.mm-t__pic { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; flex: none; }
.mm-t__pic--none { background: #dfe6e0; }
.mm-t__who { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.mm-t__line { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.mm-empty { background: var(--wash); border-radius: 12px; padding: 22px; text-align: center; }
.mm-empty p { color: var(--muted); font-size: 14px; margin: 6px 0 0; }

/* THE ASKING IS THE SCREEN, so it is a panel and not a disclosure. It used to be a <details>
   below the list; now it leads, it is open, and it has the weight of the thing the page is for.
   Its own surface rather than bare page, because a form floating on the wash reads as an
   afterthought next to the cards underneath it. */
.mm-new { margin: 18px 0 10px; background: var(--bg); border-radius: 20px; padding: 24px 24px 22px;
  box-shadow: 0 1px 2px rgba(16,24,40,.05), 0 8px 22px rgba(16,24,40,.05); }
.mm-new__h { margin: 0 0 4px; font: 700 19px/1.25 var(--font-head, system-ui);
  letter-spacing: -.015em; color: var(--ink, var(--ink)); }
/* Kept so an older cached page that still renders <details> is not left with a broken control. */
.mm-new > summary { cursor: pointer; font-weight: 600; padding: 10px 0; }

/* The record, under the asking. A heading because it is now a second section rather than the
   page's opening content, and a count because "how many have I got open" is the question a
   parent actually brings back to this screen. */
.mm-reqs__h { display: flex; align-items: center; gap: 9px; margin: 28px 0 12px;
  font: 700 15px/1.2 var(--font-head, system-ui); color: var(--ink, var(--ink)); }
.mm-reqs__n { font: 700 11px/1 var(--font-body, system-ui); color: var(--muted);
  background: var(--line2, var(--cream)); border-radius: 999px; padding: 4px 8px; }
.mm-note--cap { margin-top: 18px; }

/* ROOMIER. 12px gutters and 9px field padding is a form squeezed into a disclosure panel; given
   the width of the page it can breathe. Labels a size up, fields a size up, and a real gap
   between rows so the eye can find the next question. */
.mm-form { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 20px; margin-top: 16px; }
.mm-f { display: flex; flex-direction: column; gap: 7px; font-size: 14px; }
.mm-f--wide, .mm-form__act { grid-column: 1 / -1; }
.mm-f > span { color: var(--muted); font-weight: 600; font-size: 13.5px; }
.mm-f em { font-weight: 400; color: var(--muted); font-style: normal; }
/* Fields separate by fill, like every other input in the app — no outline. */
.mm-f input, .mm-f select, .mm-f textarea { background: var(--wash); border: 0; border-radius: 11px;
  padding: 12px 14px; font: inherit; color: inherit;
  transition: background .16s ease, box-shadow .16s ease; }
.mm-f textarea { min-height: 96px; resize: vertical; }
.mm-f input:focus, .mm-f select:focus, .mm-f textarea:focus { background: #e4eae5; outline: none;
  box-shadow: 0 0 0 3px rgba(95,129,73,.18); }
@media (prefers-reduced-motion: reduce) { .mm-f input, .mm-f select, .mm-f textarea { transition: none; } }
.mm-form__act { display: flex; align-items: center; gap: 12px; }
@media (max-width: 640px) { .mm-form { grid-template-columns: 1fr; } }
/* Invites sit above the teacher list: something to act on outranks someone to consider. */
.mm-inv__list { display: grid; gap: 8px; margin-top: 12px; }
.mm-inv { display: flex; align-items: center; gap: 10px; background: var(--cream); border-radius: 10px; padding: 10px 12px; }
.mm-inv__who { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.mm-inv__line { font-size: 12px; color: var(--muted); }
.mm-pick { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; width: 100%; margin-top: 8px; }
.mm-pick__sel { background: var(--wash); border: 0; border-radius: 8px; padding: 8px 10px; font: inherit; flex: 1; min-width: 180px; }
/* The no-pod-yet explanation inside the invite picker. Full width so it reads as a sentence
   rather than as a label squeezed beside the button — .mm-pick is a wrapping flex row. */
.mm-pick__none { flex: 1 0 100%; margin: 0 0 8px; font-size: 13px; line-height: 1.5; color: var(--muted, var(--muted)); }

/* THE MATCHMAKING LOCK — a price, not a fault, so it must not look like .mm-err.
   Tinted rather than red, the padlock rather than an alert, and it ends in the one action that
   resolves it. It replaces the Share button in the card's footer, so it inherits that row's
   full width and sits where the teacher was already looking. */
.mm-lock { flex: 1 0 100%; display: flex; align-items: flex-start; gap: 12px; margin-top: 10px;
  padding: 14px 16px; border-radius: 14px; background: var(--purple-tint, var(--cream)); }
.mm-lock__ico { flex: 0 0 auto; margin-top: 1px; color: var(--purple-ink, var(--muted)); }
.mm-lock__main { flex: 1 1 auto; min-width: 0; }
.mm-lock__t { display: block; font-size: 14px; color: var(--ink); }
.mm-lock__b { margin: 4px 0 0; font-size: 13px; line-height: 1.5; color: var(--muted); }
.mm-lock__cta { flex: 0 0 auto; align-self: center; white-space: nowrap; }
@media (max-width: 560px) {
  .mm-lock { flex-wrap: wrap; }
  .mm-lock__cta { width: 100%; text-align: center; margin-top: 4px; }
}

/* THE INVITE PICKER'S VISIBLE STATE.
   The click removed the button and then awaited myInvitablePods, so the row was empty for as
   long as that took and a teacher could not tell whether anything had happened at all -- Victor,
   from UAT: "I had no clue what was happening." Three states now have a look: waiting for the
   pod list, the invite in flight, and the result landing.

   cc-spin already exists in this file (used by the cancel flow), so the keyframes are reused
   rather than duplicated under a new name. */
.mm-spin { display: inline-block; width: 13px; height: 13px; margin-right: 6px; vertical-align: -2px;
  border: 2px solid rgba(109, 75, 216, .25); border-top-color: var(--muted); border-radius: 50%;
  animation: cc-spin .7s linear infinite; }
.mm-pick--wait { font-size: 13px; color: var(--muted, var(--muted)); }
/* Entering: the picker is new content in a row the reader is already looking at, so it arrives
   rather than appearing. */
.mm-pick { opacity: 1; transition: opacity .18s ease, transform .18s ease; }
.mm-pick.is-in { animation: mm-rise .22s ease both; }
/* In flight: the SELECT stayed live and clickable while the invite was sending, which reads as
   nothing having happened. Dimmed and inert until it resolves. */
.mm-pick.is-busy { opacity: .6; pointer-events: none; }
.mm-pick.is-done { animation: mm-pop .26s ease both; }
@keyframes mm-rise { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes mm-pop  { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }
/* Motion is the feedback here, so reduced-motion keeps the STATE change and drops the movement
   rather than dropping the signal. */
@media (prefers-reduced-motion: reduce) {
  .mm-pick.is-in, .mm-pick.is-done { animation: none; }
  .mm-spin { animation-duration: 2s; }
}

/* Said before they type, not after they are refused. */
.mm-hint { font-size: 12px; color: var(--muted); font-style: normal; line-height: 1.45; }


/* ---- Package switch confirmation ------------------------------------------
   Shown before a change to a recurring charge, with Stripe's own figures in it. Deliberately
   plainer than the checkout modal: nothing is being collected here, only agreed to. */
/* The approve-all confirmation. Same furniture as .pkgsw — a teacher should not have to learn a
   second kind of dialog — but its own name, because it is not about packages and the next person
   to restyle one should not silently restyle the other. */
.cb-ask { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center;
  justify-content: center; padding: 20px; background: rgba(18, 26, 38, .55); }
.cb-ask__box { position: relative; width: 100%; max-width: 460px; background: var(--bg, var(--bg));
  border-radius: 18px; padding: 24px; box-shadow: 0 24px 60px rgba(0, 0, 0, .28);
  display: grid; gap: 10px; }
.cb-ask__x { position: absolute; top: 12px; right: 14px; border: 0; background: none;
  font-size: 22px; line-height: 1; color: var(--muted); cursor: pointer; }
.cb-ask__title { font-family: var(--head); font-size: 19px; padding-right: 28px; }
.cb-ask__body { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink); }
.cb-ask__flag { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--muted);
  background: var(--cream); border-radius: 10px; padding: 9px 12px; }
.cb-ask__acts { display: flex; gap: 10px; justify-content: flex-end; margin-top: 6px; flex-wrap: wrap; }
/* The safe answer is the wide one. "Publish anyway" is deliberately the plainer of the two. */
.cb-ask__acts .cb-ask__no { flex: 1 1 auto; }

.pkgsw { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center;
  justify-content: center; padding: 20px; background: rgba(18, 26, 38, .55); }
.pkgsw__box { position: relative; width: 100%; max-width: 420px; background: var(--bg);
  border-radius: 18px; padding: 24px; box-shadow: 0 24px 60px rgba(0, 0, 0, .28);
  display: grid; gap: 4px; }
.pkgsw__x { position: absolute; top: 12px; right: 14px; border: 0; background: none;
  font-size: 22px; line-height: 1; color: var(--muted); cursor: pointer; }
.pkgsw__title { font-family: var(--head); font-size: 19px; font-weight: 600; color: var(--ink); }
.pkgsw__sub { font-size: 13px; color: var(--muted); }
.pkgsw__lines { margin-top: 14px; display: grid; gap: 8px; padding: 14px;
  border-radius: 12px; background: var(--line-soft, var(--cream)); }
.pkgsw__row { display: flex; justify-content: space-between; gap: 14px; font-size: 13.5px; }
.pkgsw__row span { color: var(--muted); }
.pkgsw__row b { font-variant-numeric: tabular-nums; }
/* A credit is money coming back. Reading it in the same ink as a charge is how somebody
   misreads the total in the direction that costs them. */
.pkgsw__cr { color: var(--green-ink); }
.pkgsw__row--tot { margin-top: 2px; padding-top: 10px; font-size: 15px;
  box-shadow: inset 0 1px 0 var(--line-2); }
.pkgsw__row--tot span { color: var(--ink); font-weight: 600; }
.pkgsw__row--after span, .pkgsw__row--after b { color: var(--muted); font-weight: 600; font-size: 12.5px; }
.pkgsw__note { margin: 12px 0 0; font-size: 13px; line-height: 1.5; color: var(--muted); }
.pkgsw__acts { margin-top: 18px; display: flex; gap: 10px; justify-content: flex-end; }


/* ---- Pod picker (pod-picker.js) -------------------------------------------
   Search over a panel of cover cards, collapsing to one row once a pod is chosen. Picking is a
   thing you do once; leaving a search box and fifty results up for the rest of the session
   spends the page on a decision already made. */
.podpick { display: grid; gap: 8px; min-width: 0; }
/* The real <select>. Visually hidden, not display:none, so it stays a working fallback and keeps
   submitting with the form if the card layer ever fails to draw. */
.podpick__native { position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.podpick__l { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.podpick__s { width: 100%; padding: 11px 14px; font-size: 15px; border-radius: 12px;
  border: 1px solid var(--line-2); background: var(--bg); color: var(--ink); }
.podpick__s:focus { outline: none; border-color: var(--green-ink);
  box-shadow: 0 0 0 3px var(--green-tint); }

.podpick__list { max-height: 340px; overflow-y: auto; display: grid; gap: 6px;
  padding: 6px; border-radius: 14px; background: var(--line-soft, var(--cream)); }
.podpick__o { display: flex; gap: 11px; align-items: center; width: 100%; text-align: left;
  padding: 8px; border: 0; border-radius: 10px; background: transparent; cursor: pointer; }
.podpick__o:hover { background: var(--bg); }
.podpick__o:focus-visible { outline: 2px solid var(--green-ink); outline-offset: -2px; }
.podpick__o[aria-selected="true"] { background: var(--green-tint); }
.podpick__img { flex: 0 0 auto; width: 56px; height: 42px; border-radius: 8px;
  object-fit: cover; display: block; }
.podpick__img--none { display: block; }
.podpick__txt { display: grid; gap: 1px; min-width: 0; }
.podpick__t { font-size: 14px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.podpick__m { font-size: 12.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.podpick__empty { margin: 10px; font-size: 13px; color: var(--muted); }

/* Chosen state: the search and the panel go, one row stays. */
.podpick.is-chosen .podpick__pick { display: none; }
/* Shown by the CLASS, not by the hidden attribute. `display: grid` on the base rule silently
   defeats [hidden] — the UA's display:none loses to any authored display — which is exactly how
   the "Choose a different pod" row shipped visible above a list where nothing had been chosen. */
.podpick__chosen { display: none; }
.podpick.is-chosen .podpick__chosen { display: grid; gap: 8px; padding: 10px; border-radius: 14px;
  background: var(--green-tint); }
.podpick__chosen-card { display: flex; gap: 11px; align-items: center; min-width: 0; }
.podpick__chosen-acts { display: flex; gap: 14px; align-items: center; }
.podpick__again, .podpick__edit { border: 0; background: none; padding: 0; cursor: pointer;
  font: 600 12.5px var(--sans); color: var(--green-ink); text-decoration: underline; }

/* ---- Pod Builder: the picking phase -----------------------------------
   Two columns of its own: the pod list on the LEFT, and everything about the choice — the facts
   panel, the content options, Draft — on the RIGHT. One stacked column put the options a full
   screen below the list, so choosing and configuring read as two pages joined end to end; side
   by side they are one decision seen whole, and the Draft button is on screen from the start.

   The drafted phase is untouched: cb-rail__rest is display:contents there, so the wrapper
   vanishes and the rail stacks exactly as it always did. */
.cb-rail__rest { display: contents; }
/* CENTRED IN THE WORKSPACE, not pinned to the top of it. .cb-work is a full viewport-height grid
   because the DRAFTED phase needs it — the document pane fills the screen and does the scrolling.
   The picking phase inherits that height but has only one short card in it, so the card sat at
   the top with a third of the screen empty underneath, reading as a page that had failed to
   finish loading rather than a page with one thing to do.

   SAFE centre, not plain centre. A grid item taller than its container and centred overflows
   equally in BOTH directions, and the part above the top edge cannot be scrolled back to — the
   search field would be unreachable on a short laptop screen. `safe` falls back to start exactly
   when that would happen, so it centres when there is room and never hides the top when there
   is not. */
.cb-work--pick { grid-template-columns: minmax(0, 1fr); align-content: safe center;
  /* Centred, then nudged up. Optically centred and mathematically centred are not the same
     thing: the page already carries a heading above this card and nothing below it, so a card
     sitting on the exact midpoint reads as low. The padding shrinks the box the card is centred
     IN, which lifts it by half the padding — and because everything here is border-box, the
     workspace keeps its height and the page gains no scroll. */
  padding-bottom: clamp(24px, 8vh, 100px); }
.cb-work--pick .cb-stage { display: none; }
.cb-work--pick .cb-rail {
  position: static; max-height: none; overflow: visible;
  display: grid; align-items: start;
  gap: 20px;
  /* width:100% IS LOAD-BEARING, and its absence is the whole bug.
     
     `margin-inline: auto` on a GRID ITEM does not centre a stretched box the way it does in flow
     layout — auto margins make the used width fit-content and then absorb the leftover space. So
     the card sized itself to its own contents (428px inside a 571px column) and sat centred with
     big empty gutters, looking for all the world like a viewport problem. It is why four rounds
     of breakpoint changes did nothing: auto-fit was correctly reading a card that had already
     given up 143px it was entitled to.
     
     A definite width restores the stretch; max-width still caps it; the auto margins then centre
     only the genuine leftover on windows wider than 1060. */
  /* WIDER, because the picking phase is the only thing on the screen. 1060 was measured against
     a card that had to sit politely beside nothing; on a normal laptop it left a band of page
     down each side and the pod list scrolling inside a column narrower than it needed. The cap
     still exists — past ~1320 the two columns stop being a pair and become two far-apart
     things — it is just set where the card stops gaining rather than where it started. */
  width: 100%; max-width: 1320px; margin-inline: auto;
  /* TWO COLUMNS WHENEVER THE CARD HAS ROOM FOR THEM, decided by the CARD and not by the window.
     
     This was 11fr/9fr with a `@media (max-width: 900px)` collapsing it to one column, and that
     media query is what kept putting "What should it contain?" underneath the pod facts instead
     of beside them: it reads the VIEWPORT, which includes the sidebar and both page gutters, so a
     ~830px window tripped it while the card itself still had 570px — comfortably enough for two.
     
     auto-fit measures the space actually available to these children. Two columns from about
     540px of card, one below that, and no number anywhere that has to be kept in sync with how
     wide somebody's browser happens to be. min(100%, 260px) is what stops a 260 minimum from
     overflowing a container narrower than 260. */
  /* 240, not 260, and the 20px difference is the whole thing at a narrow window: 260 x 2 plus the
     20px gap needs 540 of content box, and a ~830px browser leaves 527. Twenty pixels short is
     still one column, and still looks like the bug. 240 clears it with room to spare. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}
.cb-work--pick .cb-rail__rest { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
/* The list is the left column's whole job, so it gets the height the viewport can give —
   bounded, because past ~someone's screen it is the old endless scroll again. */
.cb-work--pick .podpick__list { max-height: min(66vh, 620px); }
/* One column again where two cannot fit. */
@media (max-width: 900px) {
  /* The column collapse that lived here is gone — auto-fit above decides it from the card's own
     width. Only the list height is a genuine viewport question: it is capped in vh, and on a
     short window the list has to give up room whatever the card is doing. */
  .cb-work--pick .podpick__list { max-height: 300px; }
  /* Stacked, the two columns become one tall column that fills the screen on its own. Centring
     a thing that already overflows only costs the top of it. */
  .cb-work--pick { align-content: start; }
}


/* ---- Redraft one piece (course-editor.js) ---------------------------------
   Sits UNDER the field it rewrites, not in the ribbon. The ribbon is for things done to text you
   are writing; this replaces the text wholesale, and putting it beside Bold invites the click
   that throws away an edit. */
/* ONE FIELD, NOT A FIELD AND A BUTTON. A bordered input with a separate tinted button beside it
   read as a form row — the same shape as "Marks out of" a few lines below — and a teacher asking
   for a rewrite in their own words is not filling in a form. The two now share one rounded
   container that takes the focus ring as a unit, which is the shape every AI composer has settled
   on and the reason it reads as somewhere to talk rather than somewhere to type a value. */
.cb-rd { display: grid; gap: 6px; }
/* AN ANIMATED GRADIENT EDGE, in the two brand colours.
   
   Two backgrounds on one element: a flat white clipped to the PADDING box, and the gradient
   clipped to the BORDER box, with a transparent border between them. The border becomes a window
   onto the gradient — which is how you get a gradient border at all, since border-color takes one
   colour and border-image cannot be animated smoothly.
   
   The movement is background-position on a 300%-wide gradient, not a rotating element. A rotation
   needs a square larger than the pill and a clipping wrapper, and it wobbles at the ends of a
   shape this wide; sliding a repeating gradient is uniform along the whole edge and costs one
   compositor property.
   
   FOUR SECONDS, deliberately slow. This sits under a document a teacher is reading, and anything
   quick in the corner of the eye pulls attention off the thing they are meant to be checking. */
.cb-rd__row { display: flex; align-items: center; gap: 8px;
  border: 3px solid transparent; border-radius: 999px;
  padding: 4px 4px 4px 18px;
  background:
    linear-gradient(var(--bg), var(--bg)) padding-box,
    /* SATURATED, not the brand tints. --purple and --green are surface colours, chosen to sit
       quietly behind text, and a 3px ring of them read as grey from a foot away. These are the
       same two hues taken to full strength — the brand relationship survives, the muting does
       not, and this is the one control on the page that is meant to be found. */
    linear-gradient(90deg,
      #7C3AED, #A855F7, #22C55E, #84CC16, #A855F7, #7C3AED) border-box;
  background-size: 100% 100%, 300% 100%;
  background-position: 0 0, 0 0;
  animation: cbedge 4s linear infinite;
  transition: box-shadow .18s ease, transform .18s ease; }
/* focus-within, so the lift belongs to the composer and not to whichever half has the caret. */
.cb-rd__row:focus-within { box-shadow: 0 6px 20px rgba(106, 84, 181, .18); transform: translateY(-1px); }
@keyframes cbedge { to { background-position: 0 0, 300% 0; } }
/* Spent, so nothing about it should still be inviting. */
.cb-rd[data-left="0"] .cb-rd__row { animation: none; background: #fff padding-box,
  linear-gradient(90deg, #dfe4e6, #dfe4e6) border-box; box-shadow: none; }
.cb-rd__note { flex: 1 1 auto; min-width: 0; border: 0; background: none;
  padding: 10px 0; font: inherit; font-size: 14px; color: var(--ink); }
.cb-rd__note:focus { outline: none; }
.cb-rd__note::placeholder { color: #97a0a9; }
/* FILLED, not tinted. This is the primary action of the strip it sits in, and a pale green button
   on a pale green field is a suggestion rather than a control. */
.cb-rd__go { flex: 0 0 auto; border: 0; border-radius: 999px; padding: 11px 22px;
  font: 700 13.5px var(--sans); color: #fff; cursor: pointer;
  /* The button carries the same two hues as the ring around it, weighted purple so it reads as
     one control rather than a green button that happens to sit in a coloured field. */
  background: linear-gradient(120deg, #7C3AED, #A855F7 55%, #22C55E);
  background-size: 180% 100%;
  box-shadow: 0 2px 10px rgba(124, 58, 237, .3);
  transition: background-position .35s ease, box-shadow .18s ease, transform .12s ease; }
.cb-rd__go:hover:not(:disabled) { background-position: 100% 0;
  box-shadow: 0 4px 16px rgba(124, 58, 237, .42); }
.cb-rd__go:active:not(:disabled) { transform: translateY(1px); }
.cb-rd__go:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* Spent, not broken. A disabled control that looks like an error suggests something went wrong,
   when in fact the teacher used a thing they were given. */
.cb-rd__note:disabled, .cb-rd__go:disabled { opacity: .5; cursor: default; }
.cb-rd__go:disabled { background: #b9c2b3; box-shadow: none; }
/* Aligned with the text inside the pill above it, not with the pill's edge. */
.cb-rd__n { font-size: 12px; color: var(--muted); padding-left: 18px; }

/* ---- What is left, countable at a glance ------------------------------------
   This was a sentence, which is precise and has to be READ — at the bottom of a page where the
   eye is on the document. Two pips and none are different shapes; "1 of 2" and "0 of 2" are the
   same shape with a different character in it. */
/* Quiet, and always present wherever generating happens. Loud enough to be read once and never
   loud enough to be the thing you look at. */
.cb-ai-note { margin: 8px 0 0; font-size: 11.5px; color: var(--muted-2); line-height: 1.4; }
.cb-meter { display: inline-flex; align-items: center; gap: 7px; flex-wrap: wrap; }
/* Undo sits on the same line as the count, at the far end. It is a rare action and a destructive
   one to reach for by accident, so it is a quiet link rather than a button competing with
   Redraft a few pixels above it. */
.cb-rd { position: relative; }
.cb-rd__undo { margin-left: auto; flex: none; background: none; border: 0; font: inherit; font-size: 12px;
  color: var(--purple-ink, var(--muted)); cursor: pointer; padding: 2px 4px; border-radius: 6px;
  text-decoration: underline; text-underline-offset: 3px; }
.cb-rd__undo:hover:not(:disabled) { background: var(--purple-tint, var(--cream)); }
.cb-rd__undo:disabled { opacity: .5; cursor: default; text-decoration: none; }
.cb-rd__foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.cb-meter b { color: var(--ink, var(--ink)); font-weight: 700; }
.cb-meter__sep { width: 1px; height: 11px; background: var(--line-2, var(--cream)); }
.cb-pips { display: inline-flex; align-items: center; gap: 3px; }
.cb-pips i { width: 7px; height: 7px; border-radius: 999px; display: block; }
/* Spent pips stay VISIBLE rather than disappearing. A row that shrinks as you use it hides how
   many you started with, which is the number that tells you whether one left is nearly all of
   them or nearly none. */
.cb-pips i.off { background: #dfe4e6; }
.cb-pips--own i.on { background: #7C3AED; }
.cb-pips--pool i.on { background: #22C55E; }
/* Out of your own and into the shared three: the moment worth colouring, because it is the moment
   a teacher starts spending what the other sessions need. */
.cb-meter.is-pool { color: var(--muted); }
.cb-meter.is-pool b { color: var(--muted); }
.cb-meter.is-out { color: var(--muted); }
.cb-meter.is-out b { color: var(--muted); }


/* ---- Pod Builder: the live draft stage ---------------------------------
   The documents land in Firestore in batches of four while the callable runs; each row reveals
   as its document is written. Skeletons hold the shape so the page promises exactly as much as
   was asked for — N rows for N sessions — and fills them in rather than growing. */
.cb-live { display: grid; gap: 14px; }
.cb-live__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.cb-live__head b { font-size: 15px; }
.cb-live__sub { font-size: 12.5px; color: var(--muted); }
.cb-live__pct { font-size: 15px; font-weight: 800; color: var(--green-ink); font-variant-numeric: tabular-nums; }
.cb-live__el { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; margin-left: auto; }
.cb-live__bar { height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.cb-live__fill { display: block; height: 100%; width: 0; border-radius: 999px;
  background: var(--green-fill); transition: width .45s cubic-bezier(.4,0,.2,1); }
.cb-live__spin { width: 14px; height: 14px; border-radius: 50%; align-self: center;
  border: 2px solid var(--green-tint); border-top-color: var(--green-ink);
  animation: cbspin .8s linear infinite; }
.cb-live__rows { display: grid; gap: 6px; }
.cb-live__row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px;
  border-radius: 10px; background: var(--line-soft, var(--cream)); min-height: 44px; }
.cb-live__dot { margin-top: 5px; }
.cb-live__dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
  background: var(--line-2); }
.cb-live__dot--ok { background: var(--green-fill); }
/* A filled row is a small stack now: title, the session's own opening line, and the work handed
   in. Wrapping is allowed on the excerpt only — a truncated title tells a teacher nothing. */
.cb-live__t { min-width: 0; font-size: 13.5px; display: grid; gap: 2px; }
.cb-live__ti { color: var(--ink-2); }
.cb-live__ex { font-size: 12.5px; color: var(--muted); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.cb-live__asg { justify-self: start; margin-top: 3px; display: inline-flex; align-items: center;
  gap: 4px; padding: 2px 8px; border-radius: 999px; background: var(--green-tint);
  color: var(--green-ink); font-size: 11.5px; font-weight: 700; }
.cb-live__asg svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-width: 3; }
/* The waiting shimmer: a text-shaped block that reads as "coming", not as broken. */
.cb-live__sk--2 { width: 22em; max-width: 92%; height: 9px; opacity: .7; }
.cb-live__sk { display: inline-block; width: 12em; max-width: 60%; height: 11px;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--line) 25%, var(--line-2) 50%, var(--line) 75%);
  background-size: 200% 100%; animation: cbshimmer 1.4s ease infinite;
  /* THE STAGGER WAS DECLARED AND NEVER APPLIED. liveStage sets --d per row and writes a comment
     about offsetting them so the list reads as a queue with work moving through it — but nothing
     ever consumed the variable, so fifty rows pulsed in perfect unison, which is the "loading
     GRAPHIC" look that comment was written to avoid. */
  animation-delay: var(--d, 0s); }

/* ---- Where the work is happening RIGHT NOW ---------------------------------
   Documents land in fours and a long pod runs for minutes, so most of this list is legitimately
   idle. Without a mark for the one being written, twenty identical shimmering rows say "something
   somewhere is loading"; with it, the list says "it is on session nine". That is the difference
   between a progress indicator and a spinner with extra steps. */
.cb-live__row.is-next { background: var(--green-tint); box-shadow: inset 0 0 0 1px var(--green-ink); }
.cb-live__row.is-next .cb-live__sk { animation-duration: .9s; animation-delay: 0s; }
.cb-live__row.is-next .cb-live__dot { background: var(--green-fill); animation: cbpulse 1.5s ease-in-out infinite; }
/* The ring is drawn with box-shadow rather than a border so it costs no layout and can be
   animated without nudging the dot a subpixel each frame. */
@keyframes cbpulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(95, 129, 73, .45); }
  50%      { box-shadow: 0 0 0 5px rgba(95, 129, 73, 0); }
}

/* Arrival: rise and settle. Content appearing is the one moment worth animating on this page. */
.cb-live__row.is-in { background: var(--bg); box-shadow: 0 1px 3px rgba(20, 30, 45, .07);
  animation: cbrise .35s ease both; }
/* The dot lands last and slightly overshoots — the full stop on the row arriving. */
.cb-live__row.is-in .cb-live__dot--ok { animation: cbpop .34s cubic-bezier(.34,1.56,.64,1) both .1s; }
/* THE ROW ASSEMBLES, it does not appear. Title, then the session's opening line, then the work
   handed in — in the order a person reads them, about a tenth of a second apart. Slower than this
   feels broken; faster and it is one flash and there was no point animating it. */
.cb-live__row.is-in .cb-live__t > * { animation: cbrise .3s ease both; }
.cb-live__row.is-in .cb-live__t > :nth-child(2) { animation-delay: .06s; }
.cb-live__row.is-in .cb-live__t > :nth-child(3) { animation-delay: .12s; }
.cb-live__row.is-in .cb-live__t > :nth-child(4) { animation-delay: .18s; }

/* A SHEEN ON THE FILLED PART, and only while it is still running. Documents land in fours, so
   between arrivals the bar is motionless for a minute and a half at a time — the one stretch
   where a teacher starts wondering whether it has died. The sweep says "still working" without
   claiming any progress that has not happened, which a creeping bar would. */
.cb-live__fill::after { content: ""; position: absolute; inset: 0; border-radius: 999px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .55), transparent);
  background-size: 60% 100%; background-repeat: no-repeat;
  animation: cbsweep 1.8s ease-in-out infinite; }
.cb-live__fill { position: relative; overflow: hidden; }
/* Done: the sweep stops. A bar that keeps shimmering after it has finished is still saying "wait". */
.cb-live.is-done .cb-live__fill::after { display: none; }
.cb-live.is-done .cb-live__fill { animation: cbsettle .5s ease both; }
.cb-live.is-done .cb-live__spin { border-color: var(--green-ink); border-top-color: var(--green-ink);
  animation: none; }

@keyframes cbspin { to { transform: rotate(360deg); } }
@keyframes cbshimmer { to { background-position: -200% 0; } }
@keyframes cbrise { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
@keyframes cbpop { from { transform: scale(0); } 60% { transform: scale(1.35); } to { transform: scale(1); } }
@keyframes cbsweep { from { background-position: -60% 0; } to { background-position: 160% 0; } }
@keyframes cbsettle { 50% { filter: brightness(1.25); } }

/* MOTION IS THE WHOLE POINT OF THIS BLOCK, so switching it off has to leave something that still
   reports progress. Everything below kills movement and keeps state: the active row stays marked
   by colour, the bar still fills, arrivals still change the row. Nothing here is load-bearing
   animation dressed up as decoration. */
@media (prefers-reduced-motion: reduce) {
  /* The gradient stays, the travelling stops — the edge still marks this control out, without
     anything moving beside a document somebody is trying to read. */
  .cb-rd__row { animation: none; }
  .cb-live__sk, .cb-live__spin { animation: none; }
  .cb-live__row.is-in, .cb-live__row.is-in .cb-live__t > *,
  .cb-live__row.is-in .cb-live__dot--ok { animation: none; }
  .cb-live__row.is-next .cb-live__dot { animation: none; }
  .cb-live__fill::after { display: none; }
  .cb-live.is-done .cb-live__fill { animation: none; }
  .cb-live__fill { transition: none; }
}

/* THE CONTROL THE TEACHER JUST PRESSED WAS THE ONE THING THAT NEVER MOVED.
   Measured rather than guessed: with a draft in flight, getAnimations() reported 12 shimmer, 1
   spinner and 2 arrivals running — and 0 on #cb-go. Every one of them was in the STAGE. The button
   under the cursor was a flat slab reading "Drafting…", which is the element the eye is on at the
   moment of pressing and for the second after it.
   It matters most exactly where it is worst: below 900px the workspace collapses to one column and
   the stage sits BELOW the rail, so on a laptop window that slab can be the only thing on screen
   for three minutes. */
.btn.is-working { position: relative; overflow: hidden; }
/* A WORKING BUTTON IS NOT A SWITCHED-OFF BUTTON, and the shared rule could not tell them apart.
   .btn:disabled sets opacity .5 — correct for "you cannot press this", wrong for "this is running
   right now". Because opacity applies to the whole subtree, it also halved the spinner and the bar
   that were added to bring it to life, so the more signal put inside it the more of it was thrown
   away. That half-strength dark slab is exactly what a full-speed draft looked like.
   The button stays disabled — it must not be pressable, and assistive tech must still say so — it
   just stops LOOKING off. cursor: progress rather than not-allowed for the same reason: "busy" is
   the honest word for it, "forbidden" is not. */
.btn.is-working:disabled { opacity: 1; cursor: progress; }
.btn__spin { width: 13px; height: 13px; border-radius: 50%; display: inline-block;
  vertical-align: -2px; margin-right: 8px; border: 2px solid rgba(255, 255, 255, .35);
  border-top-color: #fff; animation: cbspin .8s linear infinite; }
/* THE SAME TRUTH AS THE STAGE'S BAR, ON THE BUTTON ITSELF: documents that exist in Firestore over
   sessions asked for. It does not creep on a timer — a bar that advances while nothing is happening
   is a lie that gets believed exactly once, and this one has to agree with the stage beside it. */
.btn__prog { position: absolute; left: 0; bottom: 0; height: 3px; width: 0;
  background: var(--glass); transition: width .45s cubic-bezier(.4, 0, .2, 1); }
/* Reduced motion loses the SPIN and keeps the COUNT — the label still says 3 of 8 and the bar still
   moves when a document genuinely lands. Progress is information, not decoration; only the
   perpetual rotation is decoration, and it is the part that has to go. */
@media (prefers-reduced-motion: reduce) { .btn__spin { animation: none; } }

/* HIGHLIGHTING. Six colours, chosen to stay legible under black body text rather than to be
   vivid — a highlight a teacher cannot read through is a redaction. The same six names the
   sanitiser accepts (functions/course-publish.js HIGHLIGHTS); adding one here without adding it
   there produces a colour that applies, saves, and comes back grey. */
.cb-rte mark, .cb-doc mark { background: var(--cream); color: inherit; padding: 0 2px; border-radius: 3px; }
.cb-rte mark.hl-yellow, .cb-doc mark.hl-yellow { background: var(--cream); }
.cb-rte mark.hl-green,  .cb-doc mark.hl-green  { background: #C8F0C4; }
.cb-rte mark.hl-blue,   .cb-doc mark.hl-blue   { background: #C2E4FB; }
.cb-rte mark.hl-pink,   .cb-doc mark.hl-pink   { background: #FBCFE3; }
.cb-rte mark.hl-orange, .cb-doc mark.hl-orange { background: #FFD9AE; }
.cb-rte mark.hl-purple, .cb-doc mark.hl-purple { background: #DCD0F7; }

/* The swatches. Round, so they read as colours rather than as more buttons in a row of glyphs.
   EVERY SELECTOR HERE IS PREFIXED .cb-tools, because `.cb-tools button` at line 1212 sets a grey
   background and 6px 11px padding and outranks a bare class (0,1,1 against 0,1,0). Without the
   prefix all seven swatches render as identical grey pills — which is exactly how they first
   rendered, and it looks like the colours were never written rather than like a specificity loss. */
.cb-tools .cb-hl { gap: 4px; }
.cb-tools button.cb-hl__sw {
  width: 18px; height: 18px; padding: 0; border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(20, 26, 30, .14);
  cursor: pointer;
}
.cb-tools button.cb-hl__sw:hover { transform: scale(1.12); }
.cb-tools button.cb-hl__sw:focus-visible { outline: 2px solid #5F8149; outline-offset: 2px; }
.cb-tools button.cb-hl__sw--yellow { background: var(--cream); }
.cb-tools button.cb-hl__sw--green  { background: #C8F0C4; }
.cb-tools button.cb-hl__sw--blue   { background: #C2E4FB; }
.cb-tools button.cb-hl__sw--pink   { background: #FBCFE3; }
.cb-tools button.cb-hl__sw--orange { background: #FFD9AE; }
.cb-tools button.cb-hl__sw--purple { background: #DCD0F7; }
/* Remove: a white disc with a stroke through it, so "none" is a choice in the row rather than a
   gap the teacher has to guess at. */
.cb-tools button.cb-hl__sw--none {
  background:
    linear-gradient(to bottom right, transparent 46%, #C2453B 46%, #C2453B 54%, transparent 54%),
    #fff;
}

/* ---------- Overview package-usage meters ----------
   One slim card under the stat row: what this month's package allowance has left, per metered
   perk. Same card language as .scard/.panel. Meters link to the tab that spends them.
   .um--locked = the perk's numbers while the membership is past_due: visible but grey, because
   the teacher needs to see what comes back when the card clears. */
#usage-row { margin: -8px 0 24px; }
.usage-card { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 16px 22px 18px; box-shadow: var(--shadow-xs); }
.usage-card__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.usage-card__head span { font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--muted-2); }
.usage-card__head a { font-size: 13px; font-weight: 600; color: var(--purple-ink); text-decoration: none; }
.usage-card__head a:hover { text-decoration: underline; }
.usage-card__meters { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 14px 22px; }
.um { display: block; min-width: 0; text-decoration: none; }
.um__top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.um__label { font-size: 13.5px; font-weight: 600; color: var(--ink-2); }
.um__count { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.um__count b { color: var(--ink); font-weight: 700; }
.um__inf { background: var(--green-tint); color: var(--green-ink); font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 999px; }
.um__bar { display: block; height: 6px; border-radius: 999px; background: var(--wash); overflow: hidden; }
.um__bar i { display: block; height: 100%; border-radius: 999px; background: var(--green); transition: width .3s ease; }
.um__bar i.is-out { background: #d3a13c; }  /* spent, not broken - amber, never red */
.um:hover .um__label { color: var(--ink); }
.um--locked { pointer-events: auto; }
.um--locked .um__label, .um--locked .um__count, .um--locked .um__count b { color: var(--muted-2); }
.um--locked .um__bar i { background: var(--muted-2); opacity: .45; }
@media (max-width: 640px) { .usage-card { padding: 14px 16px 16px; } }


/* PACKAGE PAUSED (dunning). A declined membership renewal freezes the package; this is the recovery
   banner on the Packages page. Warm amber fill for "needs attention" without a stroke, matching the
   no-borders rule; the two actions are Pay now (this invoice) and Update card (Stripe portal). */
.pkg-paused { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 20px;
  padding: 16px 18px; border-radius: 14px; background: var(--cream); color: var(--muted);
  box-shadow: 0 1px 2px rgba(20,21,26,.05); margin: 0 0 20px; }
.pkg-paused__txt { flex: 1 1 320px; min-width: 0; }
.pkg-paused__txt b { display: inline-flex; align-items: center; gap: 7px; font-size: 15px; color: var(--muted); }
.pkg-paused__txt b svg { width: 16px; height: 16px; }
.pkg-paused__txt p { margin: 5px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--muted); }
.pkg-paused__acts { display: flex; gap: 9px; flex-wrap: wrap; flex: 0 0 auto; }
@media (max-width: 560px) { .pkg-paused__acts { width: 100%; } .pkg-paused__acts .btn { flex: 1; } }
/* ---- Matchmaking: the weighted match ------------------------------------------------------
   The band leads with the requests that fit; the full list stays below it, because a teacher who
   is bilingual and never wrote it down would never be matched on it and still needs to find that
   family. A band that leads, not a filter that hides. */
/* THE BANDS ARE GOLD AND BLACK, which is Victor's vocabulary from the Gerald call — "51% < black
   85% < Gold" — and until now it existed only on the server. bandOf() has returned gold/black/low
   since the bands landed; the card rendered a BINARY green-or-grey off `strongMatch`, so a 51%
   and an 84% looked identical and neither looked like a band. The thresholds stay in
   match-rules.js and arrive as `r.band`: a client that decided 85 for itself would be the second
   copy of a vocabulary, which is the fault that made grade score zero on every match ever made. */
.mmq__match { font: 700 11px/1 var(--font-body, system-ui); letter-spacing: .02em;
  color: var(--ink2, var(--muted)); background: var(--line2, var(--cream));
  border-radius: 999px; padding: 6px 10px; white-space: nowrap; }
/* Gold reads as an opportunity: warm, and the only warm thing on the card. */
.mmq__match--gold { color: var(--muted); background: var(--cream); box-shadow: inset 0 0 0 1px #eedb9a; }
/* Black is a real match and deliberately NOT a second green — green is this product's
   yes/approved colour and was doing that job here while also meaning "band", which is how 51%
   and 84% came to look like the same answer. */
.mmq__match--black { color: #f2f4f6; background: #23272c; }
.mmq__match--low { color: var(--muted); background: var(--cream); }

/* The gold card carries the band at its edge too, so the best match is findable in a grid
   without reading every chip. inset, because .mmq has a shadow and no border — the old
   `.mmq.is-strong { border-color }` set a colour on a border that was never drawn, and had no
   effect on anything for as long as it has existed. */
.mmq.is-gold { box-shadow: inset 0 0 0 1.5px #ecd696,
  0 1px 2px rgba(16,24,40,.05), 0 8px 20px rgba(122,90,16,.08); }
.mmq.is-gold:hover { box-shadow: inset 0 0 0 1.5px #e6cc82,
  0 2px 4px rgba(16,24,40,.06), 0 14px 30px rgba(122,90,16,.11); }

.mm-strong { margin: 0 0 22px; }
.mm-strong__h { display: flex; align-items: center; gap: 8px; margin: 0 0 2px;
  font: 700 15px/1.2 var(--font-head, system-ui); color: var(--ink, var(--ink)); }
.mm-strong__n { font: 700 11px/1 var(--font-body, system-ui); color: var(--muted);
  background: var(--cream); border-radius: 999px; padding: 4px 8px; }
.mm-strong__lede { margin: 0 0 12px; font-size: 13px; color: var(--muted, var(--muted)); }
/* THE BAND USES THE SAME GRID AS THE LIST, deliberately. It was a horizontal rail with a fixed
   340px basis, which set a one-word-per-line headline when the band held its usual ONE card. Two
   different layouts for the same object was also why the best match and the rest never looked
   like members of one list. Same columns, same floor — the band earns its prominence from the
   card TREATMENT, not from a separate geometry. */
.mm-strong__rail { display: grid; gap: 18px;
  grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)); }

/* ---- Flashcards: the deck viewer ------------------------------------------------------------
   MOVED, NOT DELETED. Decks are rendered by one module (js/classroom/pod-classroom.js) on one page
   (pod-class.html), and that page loads styles.css and never this sheet — so this copy styled
   nothing and could only ever go stale. It did: the cards turn over in 3D now and these rules
   still described the display:none swap, which would have hidden the back face outright on any
   page that loaded them. The live rules are in styles.css under "Flashcard decks in the
   CLASSROOM". Put deck styling there. */


/* ---- Decks and games in the DRAFT EDITOR ----------------------------------------------------
   Both kinds were generated, stored and published for hours before they had any appearance here
   — the extras renderer knew three kinds and called everything else a discussion question. These
   rules dress the two payload editors. .cb-deck__n is shared by both so a card and a question
   number the same way. */
.cb-deck, .cb-game { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 8px; }
.cb-deck__row { display: grid; grid-template-columns: 22px 1fr 1fr; gap: 8px; align-items: center; }
.cb-deck__n { font-size: 11px; color: var(--muted, var(--muted)); text-align: right; }
.cb-deck__front, .cb-deck__back, .cb-game__q, .cb-game__otext {
  width: 100%; padding: 7px 9px; border: 1px solid var(--line2, #eef1f5); border-radius: 8px;
  font: inherit; font-size: 13px; background: var(--bg); min-width: 0; }
.cb-deck__front:focus, .cb-deck__back:focus, .cb-game__q:focus, .cb-game__otext:focus {
  outline: 2px solid #7C5CFF; outline-offset: 1px; }
.cb-game__row { border: 1px solid var(--line2, #eef1f5); border-radius: 10px; padding: 10px; background: var(--wash); }
.cb-game__top { display: grid; grid-template-columns: 22px 1fr; gap: 8px; align-items: center; }
.cb-game__opts { display: grid; gap: 6px; margin: 8px 0 0 30px; }
.cb-game__opt { display: grid; grid-template-columns: 18px 1fr; gap: 8px; align-items: center; }
/* The correct option is marked by FILL, not by a ring — the no-strokes rule the apps follow, and
   a green tint reads as "this is the answer" faster than a border does. */
.cb-game__opt.is-right .cb-game__otext { background: var(--cream); }

/* ---- First paint, before anything has loaded --------------------------------
   The shell renders instantly and its contents do not. The sidebar nav, the stat cards and the
   panels are each filled by an `innerHTML =` that cannot run until auth resolves, a profile read
   lands and myMembership answers - so what a teacher saw immediately after signing in was a
   heading over an empty white column. That reads as a broken app rather than a loading one, and
   it is the first thing they see.

   These are the shapes of what is coming, in the places it will appear, so the page fills in
   instead of jumping. They need no JavaScript and no removal code: every container that holds a
   skeleton is assigned innerHTML wholesale on the load path, so it is replaced by construction.
   That is also the fragile part - anything that stops assigning one of those containers leaves a
   skeleton shimmering forever, which is why dashboard.html carries a stall guard and why each
   site is commented. */
.dsk { display: flex; flex-direction: column; gap: 20px; }
.dsk__group { display: flex; flex-direction: column; gap: 2px; }
.dsk__label { height: 8px; width: 74px; margin: 0 10px 10px; border-radius: 5px; }
/* 40px is what .side__link measures: 10px padding either side of a 14.5px line. Matching it is
   the whole point - a skeleton of the wrong height moves the page when the real nav arrives. */
.dsk__row { height: 40px; border-radius: 11px; }
.dsk-card { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
            padding: 22px; box-shadow: var(--shadow-xs); }
.dsk-panel { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
             padding: 24px; box-shadow: var(--shadow-xs); }
.dsk__ico { display: block; width: 44px; height: 44px; border-radius: 13px; margin-bottom: 16px; }
.dsk__bar { display: block; height: 11px; border-radius: 6px; }
.dsk__bar--h { height: 15px; width: 42%; margin-bottom: 20px; }
.dsk__bar--n { height: 22px; width: 54%; margin-bottom: 10px; }
.dsk__bar--l { height: 10px; width: 72%; }
.dsk-panel .dsk__bar { margin-bottom: 14px; }
.dsk-panel .dsk__bar:last-child { margin-bottom: 0; width: 55%; }

/* ONE sweep, sharing cbshimmer with the pod builder's skeletons further up this file, so the
   two waits in this app never drift into two different-looking animations. */
.dsk__label, .dsk__row, .dsk__ico, .dsk__bar {
  background: linear-gradient(90deg, var(--line) 25%, var(--line-2) 50%, var(--line) 75%);
  background-size: 200% 100%; animation: cbshimmer 1.4s ease infinite;
}
/* Staggered so it reads as work moving through, not one slab pulsing in unison. */
.dsk__group .dsk__row:nth-child(3) { animation-delay: .07s; }
.dsk__group .dsk__row:nth-child(4) { animation-delay: .14s; }
.dsk__group .dsk__row:nth-child(5) { animation-delay: .21s; }
.dsk-card:nth-child(2) .dsk__ico, .dsk-card:nth-child(2) .dsk__bar { animation-delay: .09s; }
.dsk-card:nth-child(3) .dsk__ico, .dsk-card:nth-child(3) .dsk__bar { animation-delay: .18s; }
.dsk-card:nth-child(4) .dsk__ico, .dsk-card:nth-child(4) .dsk__bar { animation-delay: .27s; }
.dsk-panel:nth-child(2) .dsk__bar { animation-delay: .12s; }

/* A moving skeleton is motion the reader did not ask for. Reduced motion keeps the SHAPES - they
   are the useful half, and they still say "content is coming here" - and drops the sweep. */
@media (prefers-reduced-motion: reduce) {
  .dsk__label, .dsk__row, .dsk__ico, .dsk__bar { animation: none; background: var(--line); }
}

/* The wait that did not end. Reached only by the stall guard in dashboard.html: a skeleton still
   on screen after twelve seconds is promising something that is not coming, which is a worse lie
   than the blank page this replaced. */
.dsk-stall { grid-column: 1 / -1; margin: 0; padding: 28px 4px; text-align: center;
             color: var(--muted); font-size: 14px; }
.dsk-stall__btn { font: inherit; font-weight: 600; color: var(--purple-ink); background: none;
                  border: 0; padding: 0 2px; cursor: pointer; text-decoration: underline; }

/* ---- The free Generator week -------------------------------------------------------------- */
/* Purple, because the Generator is the AI and purple is what the membership table already uses to
   mark the packages that include it. Green would read as "approved" and this is an offer, not a
   state. Removes itself the day the week ends — see generator-trial.js. */
.gtrial { display: flex; align-items: center; gap: 12px; margin-bottom: 14px;
  padding: 13px 15px; border-radius: 14px;
  background: color-mix(in srgb, var(--purple-tint) 70%, #fff);
  border: 1px solid color-mix(in srgb, var(--purple-ink) 34%, transparent); }
.gtrial__ic { display: inline-flex; color: var(--purple-ink); flex: 0 0 auto; }
.gtrial__t { display: grid; gap: 1px; min-width: 0; }
.gtrial__t b { font-size: 14.5px; color: var(--ink); }
.gtrial__t span { font-size: 12.5px; color: var(--ink-2); }
/* The count sits right and never wraps: it is the thing being counted down. */
.gtrial__d { margin-left: auto; flex: 0 0 auto; font-weight: 700; font-size: 13px;
  color: var(--purple-ink); background: var(--bg); border-radius: 999px; padding: 5px 11px;
  white-space: nowrap; }
@media (max-width: 560px) {
  .gtrial { flex-wrap: wrap; }
  .gtrial__d { margin-left: 0; }
}

.gtrial-modal { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center;
  padding: 20px; background: rgba(15, 23, 42, .5); backdrop-filter: blur(2px); }
.gtrial-modal__card { width: min(440px, 100%); background: var(--bg); border-radius: 18px;
  padding: 26px 24px; text-align: center; box-shadow: 0 24px 60px rgba(15, 23, 42, .3); }
.gtrial-modal__ic { display: inline-flex; color: var(--purple-ink); margin-bottom: 6px; }
.gtrial-modal__card h2 { font-family: var(--head); font-size: 21px; color: var(--ink); margin: 0 0 8px; }
.gtrial-modal__card p { font-size: 14px; line-height: 1.6; color: var(--ink-2); margin: 0 0 10px; }
.gtrial-modal__sub { font-size: 12.5px !important; color: var(--muted) !important; }
.gtrial-modal__card .btn { margin-top: 6px; }

/* ---- Get Ready: the five-step tour ---------------------------------------------------------- */
/* THE DIM IS A SHADOW ON A HOLE, not a sheet with a bright shape on top. A transparent box with a
   9999px shadow spread darkens the whole viewport EXCEPT its own rectangle, which is what makes the
   spotlight land on the real element at whatever size the layout gave it. */
.gready { position: fixed; inset: 0; z-index: 95; }
.gready-on { overflow: hidden; }            /* the page must not scroll out from under the hole */
.gready__hole { position: fixed; border-radius: 12px; pointer-events: none;
  box-shadow: 0 0 0 9999px rgba(15, 23, 42, .72);
  outline: 2px solid var(--purple-ink); outline-offset: 2px;
  transition: top .34s cubic-bezier(.4, 0, .2, 1), left .34s cubic-bezier(.4, 0, .2, 1),
              width .34s cubic-bezier(.4, 0, .2, 1), height .34s cubic-bezier(.4, 0, .2, 1),
              opacity .2s; }
/* With no target the hole collapses and its own shadow becomes the full dim. */
.gready__hole[style*="opacity: 0"] { inset: 0; width: 100%; height: 100%; border-radius: 0;
  outline: 0; box-shadow: 0 0 0 9999px rgba(15, 23, 42, .72); }

.gready__card { position: fixed; left: 50%; bottom: 6vh; transform: translateX(-50%);
  width: min(560px, calc(100vw - 32px)); background: var(--bg); border-radius: 20px;
  padding: 26px 26px 20px; box-shadow: 0 30px 70px rgba(15, 23, 42, .45); }
/* Steps with no element to point at put the card in the middle — it IS the content then. */
.gready__card--mid { top: 50%; bottom: auto; transform: translate(-50%, -50%); }

.gready__card.is-in { animation: gready-in .38s cubic-bezier(.2, .8, .3, 1) both; }
@keyframes gready-in {
  from { opacity: 0; transform: translateX(-50%) translateY(14px) scale(.97); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
}
.gready__card--mid.is-in { animation-name: gready-in-mid; }
@keyframes gready-in-mid {
  from { opacity: 0; transform: translate(-50%, -46%) scale(.97); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .gready__card.is-in, .gready__card--mid.is-in { animation: none; }
  .gready__hole { transition: none; }
}

.gready__dots { display: flex; gap: 6px; margin-bottom: 12px; }
.gready__dots i { width: 22px; height: 4px; border-radius: 999px; background: var(--line-2, var(--cream)); }
.gready__dots i.is-on { background: var(--purple-ink); }
.gready__h { font-family: var(--head); font-size: 20px; color: var(--ink); margin: 0 0 8px; }
.gready__b { font-size: 14.5px; line-height: 1.6; color: var(--ink-2); margin: 0 0 18px; }
.gready__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.gready__count { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* ==========================================================================================
   THE DARK DASHBOARD — teachers, parents and students.

   Victor, 2026-09-13: "I'd like dark theme applied to the background of the dashboard now. (the
   backend views) we've done the public views PERFECTLY. Take that away as a major and your main
   reference to build the dark theme for all dashboards for all parties."

   SO IT REUSES THE PUBLIC THEME'S PALETTE RATHER THAN DEFINING A SECOND ONE. `.pc-dark body` in
   styles.css already remaps the whole token set — --ink, --ink-2, --muted, --muted-2, --bg,
   --cream, --wash, --line, --line-2, --glass, the four shadows, and the accent inks and tints.
   dashboard.css reads those tokens 427 times and auth.css 153 more, so putting `pc-dark` on
   dashboard.html repaints almost the entire surface with the values Victor already signed off on.

   I WROTE A SEPARATE `.pc-dark .dash` PALETTE FIRST AND DELETED IT. It was a second source of
   truth for the same colours, with different values — the dashboard would have drifted from the
   marketing pages the first time either was adjusted, which is the opposite of "use that as your
   main reference".

   WHAT IS LEFT IS ONLY WHAT THE DASHBOARD ADDS, and it is two things.
   ========================================================================================== */

/* ONE: THE SHELL'S OWN BACKGROUND, which is hard-coded rather than tokenised — two radial glows
   over #f5f6f8, the same construction the landing page hero uses. Same glows, same positions,
   lifted a little because a wash that reads on white disappears on near-black. */
.pc-dark .dash {
  background:
    radial-gradient(1200px 600px at 100% -10%, rgba(179,162,232,.16), transparent 60%),
    radial-gradient(1000px 560px at -5% 110%, rgba(163,200,147,.13), transparent 55%),
    var(--bg);
}

/* TWO: THE SURFACES THAT WERE ALREADY DARK, which the blanket remap breaks.
 *
 * styles.css states the flaw plainly where it hits .cta-band and .stats: a token remap assumes
 * every component sits on the PAGE background, so anything already living on a dark ground is
 * handed dark-mode tokens it never wanted — and the failure is silent, because the rule still
 * applies and the colour still resolves and the element simply disappears.
 *
 * The dashboard has its own instances. A selected sidebar link and the .btn--dark buttons are
 * near-black in BOTH themes, so their children were authored light-on-dark already; remapping
 * --ink under them would paint near-white text on near-black and near-black text on near-black by
 * turns. The tokens are put back where the ground did not change. */
.pc-dark .side__link.is-active,
.pc-dark .btn--dark,
.pc-dark .msgx-row.is-on {
  --ink: #ffffff;
  --ink-2: rgba(255,255,255,.86);
  --muted: rgba(255,255,255,.72);
}

/* THE POD CARD'S TINT, WHICH IS THE ONE COLOUR THE TOKENS CANNOT REACH.
 *
 * A .cr-pod carries its teacher-chosen colour as an INLINE background, so it is immune to a token
 * remap by construction — the card stayed pale lavender while the text on it went white, and a
 * contrast sweep across all 33 screens measured it at 1.07:1. Every pod card on every role, and
 * silent: the rule applied, the colour resolved, the words vanished.
 *
 * The fix is styles.css's own, lifted from .pc-dark .pod__body: the emitters set --pod-tint and
 * paint `background: var(--pod-tint)`, so light is unchanged, and dark mixes that same hue down to
 * 14% over the card's dark ground. The pod keeps its identity — a maths pod is still purple — and
 * the type is legible because it is sitting on near-black again. */
.pc-dark .cr-pod { background: color-mix(in srgb, var(--pod-tint, transparent) 14%, var(--cream)) !important; }
.pc-dark .cr-pod__course,
.pc-dark .cr-pod__grades,
.pc-dark .cr-pod__sched { color: var(--muted); }
.pc-dark .cr-pod__price,
.pc-dark .cr-pod h4,
.pc-dark .cr-stat b { color: var(--ink); }
.pc-dark .cr-stat span { color: var(--muted-2); }
.pc-dark .cr-tag { background: rgba(255,255,255,.10); color: rgba(255,255,255,.82); }

/* THE REST OF THE DARK SURFACE, and it is two different faults that a screenshot cannot tell
 * apart. A contrast sweep over all 33 screens separated them: read the effective background of
 * every text node, composite the colour over it, and compare. 374 failures before the pod card
 * was fixed, 130 after — and of those 130 only FOUR sat on a light ground. The rest were dim grey
 * type, authored against white, now sitting on near-black at 1.0–1.5:1.
 *
 * FOUR LIGHT GROUNDS. Every one is a container whose fill is hard-coded in a runtime-injected
 * stylesheet, so no token ever reached it. */
.pc-dark .msgx-side { background: var(--cream); border-right-color: var(--line); }
.pc-dark .msgx-row:hover { background: rgba(255,255,255,.06); }
.pc-dark .stu__pod { background: rgba(255,255,255,.07); color: var(--ink-2); }
.pc-dark .gr-kids { background: rgba(255,255,255,.07); }
.pc-dark .gr-kid { color: var(--muted); }
.pc-dark .gr-kid.is-on { background: var(--bg); color: var(--ink); }

/* AND THE AMBER PILL KEEPS ITS DARK TYPE. .vpill--warn is a light amber badge in BOTH themes —
 * it is a warning, and washing it out would be the point of it lost — so the remap must not put
 * near-white text on it. Same class of bug as .cta-band in styles.css, and the same remedy. */
.pc-dark .vpill--warn, .pc-dark .vpill--ok { color: var(--ink); }
.pc-dark .vpill--ok { color: var(--ink); }

/* DIM TYPE, BRIGHTENED. These are greys chosen against white — #6b7580, #8a94a0, #98a0aa and
 * friends — living inside components that are now near-black. They are legible again at the
 * theme's own muted token rather than at a new value invented here. */
.pc-dark .pk, .pc-dark .pk small,
.pc-dark .pa small,
.pc-dark .paysched__prog,
.pc-dark .paysched__plan,
.pc-dark .mmq__facts,
.pc-dark .mmq__note,
.pc-dark .mm-lede,
.pc-dark .ros-tab,
.pc-dark .sw2-both,
.pc-dark .msgx-row__body small,
.pc-dark .msgx-row time,
.pc-dark .pct,
.pc-dark .asg-row small,
.pc-dark .paypod__meta { color: var(--muted); }

.pc-dark .pk, .pc-dark .pa,
.pc-dark .mmq__budget,
.pc-dark .mmq__subject,
.pc-dark .pfam__h, .pc-dark .pfam__n,
.pc-dark .asg-score,
.pc-dark .asg-row strong,
.pc-dark .msgx-row__body strong,
.pc-dark .paypod__title, .pc-dark .paypod__amt,
.pc-dark .paytile b, .pc-dark .grx-top__pct { color: var(--ink); }

.pc-dark .msgx-row__ava--i { background: rgba(255,255,255,.14); color: var(--ink); }
.pc-dark .mmq__done { color: var(--green-ink); }
.pc-dark .spotx__live { color: var(--ink); }
.pc-dark .paysched__bar { background: rgba(255,255,255,.10); }
.pc-dark .vsection__sub, .pc-dark .paynote, .pc-dark .podc-hint { color: var(--muted); }

/* THE LAST FORTY-THREE, and three of them are lessons rather than colours.
 *
 * .mmq__best got DARK type from the rule above because I grouped it with the amber pills — and it
 * has no light ground at all, it is a gold WORD on the dark card. Grouping by how a thing looks
 * instead of by what it sits on is exactly the mistake this whole section is about, made while
 * fixing it.
 *
 * .pct and the two bold marks needed a more specific selector, not a different value: `.pc-dark
 * .pct` loses to `.grx-pod__grade .pct`, which is (0,2,0). Specificity, not source order — moving
 * the rule further down the file would have changed nothing. */
.pc-dark .mmq__best { color: #e9c46a; }
.pc-dark .grx-pod__grade .pct { color: var(--muted); }
.pc-dark .grx-pod__grade b { color: var(--ink); }
.pc-dark .paysched__head strong { color: var(--ink); }
.pc-dark .grx-pod__nograde { color: var(--muted-2); }

/* LIGHT MODE GETS ITS ORIGINAL SHADES BACK, EXACTLY.
 *
 * The token pass above rewrote ~148 hard-coded colours into var() so the dark theme could reach
 * them. Most mapped onto a token whose light value was byte-identical — background:#fff onto
 * --bg:#ffffff — but several collapsed NEAR-identical shades onto one token, and that is a change
 * to light mode that nobody asked for:
 *
 *     #f5f6f8 -> --cream  #f6f5f2      the page wash
 *     #14202b -> --ink    #0f172a      body ink
 *     #8a94a0 -> --muted  #64748b      caption grey
 *
 * Nobody could see the difference and two of the three slightly IMPROVE contrast, which is exactly
 * the reasoning that lets an unrequested change ship. Victor's instruction was to build the dark
 * theme, not to adjust the light one, and "you would not notice" is not the same as "you approved
 * it".
 *
 * So the dashboard's own light values are restored here, scoped to the LIGHT state only. Dark is
 * untouched — `:root:not(.pc-dark)` cannot match when the class is present — and the substitution
 * keeps its whole benefit, because the tokens still carry the dark values when it counts. */
:root:not(.pc-dark) .dash,
:root:not(.pc-dark) .preview-shell,
:root:not(.pc-dark) main.podw-page {
  --cream: #f5f6f8;
  --ink: #14202b;
  /* THE GREY IS THE DARKEST OF THE ONES IT REPLACED, NOT THE COMMONEST.
     The sweep flattened about ten distinct caption greys onto this one token — #8a94a0, #6b7580,
     #86868b, #6e6e73, #98a0aa and friends. Any single value makes some text lighter than it was,
     and lighter on white means LESS contrast: picking the most common one (#8a94a0) measured 60
     MORE failures in light than the file had before I touched it. #6b7580 is the darkest of the
     group, so no caption on this screen is less legible than it was, and several are more. */
  --muted: #6b7580;
  --muted-2: #8a94a0;
}

/* The theme control, in the dashboard's top bar beside Create New Pod.
   It reuses .theme-toggle from styles.css — the marketing header's own control — so the two are
   one component rather than two that look alike; only the row it sits in is defined here. */
.dash__topacts { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dash__top .theme-toggle { flex: 0 0 auto; }

/* ==========================================================================================
   THE BACKGROUNDS A COLOUR SWEEP STRUCTURALLY CANNOT REACH.
   Victor, six reports in a row, all of them real and all of them the same shape.

   The luminance sweep rewrites a declaration whose VALUE IS A COLOUR. Three kinds are not:

     a GRADIENT       `linear-gradient(180deg,#fbfaff,#fff)` — the value is a function, so the
                      sweep skips it correctly. My Students' three summary tiles are this, and
                      they rendered as white slabs with white numerals on them.
     a SHORTHAND      `background:#fff url("data:image/svg+xml,…")` — the arrow on a <select>.
                      The Your-pods SORT / SUBJECT / TYPE boxes are this.
     an RGBA          `rgba(255,255,255,.9)` — not a hex, so not matched at all. The Messages
                      conversation header is this one.

   And a fourth that is not a declaration anywhere: a NATIVE FORM CONTROL with no background
   rule takes the browser's own white. No amount of sweeping the stylesheets finds that, because
   there is nothing in them to find.

   Each is overridden rather than rewritten. Rewriting a gradient means choosing new stops for a
   theme the original author never had in mind; naming the token here keeps one source for the
   colour and leaves the light design exactly as it was.
   ========================================================================================== */

/* LIGHT GRADIENTS — flat token fills in dark, so nothing reads as a white slab. */
.pc-dark .ros-sum,
.pc-dark .panel__empty,
.pc-dark .pe-rv__earn,
.pc-dark .incx__selbar,
.pc-dark .incx__grouph,
.pc-dark .sw2-card--both .sw2-src,
.pc-dark .podc-quizcard,
.pc-dark .podc-quizcard--done { background: var(--cream); }
/* These two carry MEANING in their colour — a review is amber, a best match is gold — so they
   keep the hue and lose the brightness rather than going flat. */
.pc-dark .sh-review { background: linear-gradient(120deg, rgba(233,196,106,.16), transparent 70%); }
.pc-dark .mmq.is-gold { background: radial-gradient(130% 90% at 50% -25%, rgba(233,196,106,.18) 0%, transparent 60%), var(--cream); }

/* THE RGBA HEADER. Victor: "fix the banner in message". */
.pc-dark .msgx-head { background: var(--cream); }

/* NATIVE FORM CONTROLS. The <select> keeps its custom arrow; only the colour behind it moves,
   and the arrow's own fill is lightened to stay visible. `color-scheme: dark` is what makes the
   OPEN dropdown list dark too — that list is drawn by the operating system, not by this page,
   and it is the one part of a <select> no CSS of ours can style. */
.pc-dark .dash, .pc-dark .podc, .pc-dark body { color-scheme: dark; }
.pc-dark select, .pc-dark input, .pc-dark textarea { background: var(--cream); color: var(--ink); }
.pc-dark select option { background: var(--cream); color: var(--ink); }
.pc-dark .cr-filt__sel {
  background: var(--cream) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23b9c0ca' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E") no-repeat right 10px center;
  border-color: var(--line);
}
.pc-dark input::placeholder, .pc-dark textarea::placeholder { color: var(--muted-2); }

/* THE PACKAGES CHIP SAT UNDER ITS OWN CARD'S OUTLINE. Victor: "the chip needs to be over the
   outline". It is positioned on the card's top edge, and the card's border paints over it —
   a stacking-order problem, not a colour one, so it needs a layer and an opaque ground of its
   own rather than a lighter background. */
.perk-pkg__tag { position: relative; z-index: 2; }
.pc-dark .perk-pkg__tag { background: var(--cream); }

/* THE SOFT BUTTON. Victor: "in match making the button is white or too light".
 *
 * `.btn--soft` is `--purple-tint` on `--purple-ink` — a pale lavender pill with purple type,
 * which is a good light-mode button and, on dark, pale-on-pale. The dark token set already
 * carries the inverted pair (--purple-tint goes translucent, --purple-ink goes light), so the
 * fault is only the hard-coded `#e9e1fa` that dashboard.css paints over it inside .mmq__foot.
 * Removing that one override lets the tokens do what they already know how to do. */
.pc-dark .mmq__foot .btn--soft { background: var(--purple-tint); color: var(--purple-ink); }
.pc-dark .mmq__foot .btn--soft:hover { background: rgba(179,162,232,.30); }
.pc-dark .btn--soft:hover { background: rgba(179,162,232,.30); }

/* THE POD VIEW'S PAGE. Victor, twice: "fix this in pod view…background needs to be dark theme",
 * then "fix pod view" with the whole shell still light around dark content.
 *
 * THE FIRST ATTEMPT MISSED BECAUSE I GUESSED AT THE CULPRIT. I scoped it to `.section`, which is
 * the marketing shell the page sits in, and the page was still light — because the classroom
 * paints its OWN surface: `main.podw-page { background: #f5f5f7 }`, and that rule lives in
 * styles.css, the one file I had deliberately excluded from every colour pass because it owns the
 * marketing palette. Excluding it was right for the marketing rules and wrong for the ~130
 * classroom declarations that happen to share the file.
 *
 * Those are tokenised now, scoped to `.podc*` / `.podw*` selectors only — classes that appear in
 * no marketing page and no marketing module, which is checked rather than assumed. The light
 * values are restored exactly above, the same way the dashboard's are. */
.pc-dark .podc, .pc-dark .podc-shell { background: transparent; }
.pc-dark .section { background: transparent; }
.pc-dark .section--tint { background: var(--cream); }
.pc-dark main.podw-page { background: transparent; }

/* THE PRIMARY BUTTON WAS BLACK ON BLACK. Victor: "create a pod need a grey color button for next".
 *
 * `.btn--green` is `background: var(--black)` — the product's dark anchor button, named green for
 * historical reasons — and in dark mode the PAGE is var(--black) too. #14151a on #14151a: the
 * label floated as bare text with no button under it, which is exactly what his screenshot shows.
 *
 * styles.css already met this and answered it with a hairline — `.pc-dark .hero .btn--green` gets
 * `border: 1px solid rgba(255,255,255,.34)` — but scoped to the hero and the site header, so
 * nothing in the product inherited it.
 *
 * A RAISED GREY, not a border, because he asked for a grey button and because an outline-only
 * button reads as secondary. It sits two steps off the page so it is unmistakably a control, and
 * the hover goes lighter rather than darker — on a dark ground, "lighter" is what forward means. */
.pc-dark .btn--green,
.pc-dark .btn--dark {
  background: #33353d;
  color: #fff;
  border: 1px solid rgba(255,255,255,.14);
  box-shadow: 0 10px 24px rgba(0,0,0,.45);
}
.pc-dark .btn--green:hover:not(:disabled),
.pc-dark .btn--dark:hover:not(:disabled) { background: #41444d; box-shadow: 0 14px 30px rgba(0,0,0,.55); }
/* The marketing pages keep their own treatment — they are already correct and Victor said so. */
.pc-dark .hero .btn--green, .pc-dark .site-header .btn--green { background: var(--black); box-shadow: none; }

/* A TEXT TOKEN USED AS A FILL INVERTS UNDER THE REMAP, AND INVERTS SILENTLY.
 *
 * Victor's classroom sidebar had a WHITE "Home" pill with white text on it — 1:1, the words
 * simply gone. The rule is `.podw-nav .podc-tab.is-active { background: var(--ink) }`: perfectly
 * sensible in light, where --ink is near-black and makes a selected pill read as filled. Dark
 * remaps --ink to #ffffff because it is the TEXT colour, so the fill flips to white while the
 * label on it stays white.
 *
 * styles.css already documents this shape for .cta-band and .stats, and it had already been
 * handled for .subject-pick.is-on and .hgl-dot.is-on. A sweep for `background: var(--ink…)` found
 * five more with no override at all — the ones below. --ink-fill is NOT remapped, so every rule
 * using THAT is already safe and is deliberately left alone.
 *
 * The replacement is a raised grey rather than a flipped token: on a dark ground a selected thing
 * reads as LIGHTER than its surroundings, which is the same logic as the primary button above. */
.pc-dark .podw-nav .podc-tab.is-active,
.pc-dark .dash__tab.is-active,
.pc-dark .dash__tabsback { background: #33353d; color: #fff; border-color: rgba(255,255,255,.14); }
.pc-dark .podc-meetbtn--setup { background: #33353d; color: #fff; }
/* The stat tooltip is a floating panel, not a pill — it wants to sit ABOVE the page, not on it. */
.pc-dark .scard__tip { background: #2a2c33; color: #fff; border: 1px solid rgba(255,255,255,.14); }

/* THE CLASSROOM'S ACCENT TEXT, LIGHTENED FOR A DARK GROUND.
 *
 * Victor: "look all throughout the pod view … validate the colors are good and text adapts."
 *
 * The colour sweep deliberately leaves saturated colours alone, because they carry meaning — red
 * is an error, green is done, purple is a link, amber is a warning — and flattening them to grey
 * destroys the signal. That is right, and it is only half the job: those accents were all chosen
 * to sit on WHITE. #1f7a45 on white is a confident green; on #14151a it is a smudge.
 *
 * So the hue survives and the lightness moves. The greens and purples map onto --green-ink and
 * --purple-ink, which the dark token set already inverts (#a3c893 / #cabff2) — the same pair the
 * marketing pages use, so the classroom agrees with the rest of the product rather than inventing
 * its own light green. Red and amber have no token, so they get the equivalent treatment.
 *
 * NOT swept, deliberately: the same colours used as a BACKGROUND. A red pill with white text is
 * correct in both themes; only the cases where the accent IS the text are here. */
.pc-dark .qzr--ok, .pc-dark .qzr--ok .qzr__pts,
.pc-dark .qzr__key, .pc-dark .qzr__ch li.is-key { color: var(--green-ink); }

.pc-dark .podc-fc__back, .pc-dark .podc-syl__go,
.pc-dark .podc-reply-btn, .pc-dark .podc-icobtn:hover,
.pc-dark .qzr__note, .pc-dark .podc-chip--ghost .podc-spin,
.pc-dark .podc-back { color: var(--purple-ink); }

/* Red and amber have no inverted token, so they take the same treatment by hand: the hue kept,
   the lightness raised until it reads on near-black. */
.pc-dark .podc-del-btn, .pc-dark .podc-post__tools .podc-del,
.pc-dark .podc-reply__x:hover, .pc-dark .podc-cwgroup__h.is-overdue,
.pc-dark .podc-gb__late, .pc-dark .podc-report:hover,
.pc-dark .podc-aibud--out svg, .pc-dark .podc-aibud--out span,
.pc-dark .wb-dlg__err, .pc-dark .podn__stat--warn b,
.pc-dark .qzr--no, .pc-dark .qzr--no .qzr__pts { color: #f0928a; }

.pc-dark .podc-aibud--low svg, .pc-dark .podc-aibud--low span { color: #e9c46a; }

/* AND THE TINTED CHIPS KEEP THEIR DARK TEXT, because they keep their LIGHT tint.
 *
 * I lightened these along with the rest and measured the result: .podc-pill--new came out at
 * 1.44:1, .podc-pill--seen at 1.65:1 — light type on a light pill, which is the same fault I had
 * just spent the evening removing, introduced by the fix for it.
 *
 * The reason is worth keeping. A saturated tint is an ACCENT SURFACE and the sweep correctly left
 * it alone: a green "Seen" pill should still be green on a dark page, exactly as the amber warning
 * pill is. So the pill does not move, and therefore its text must not move either. The rule is not
 * "lighten accent text in dark" — it is "lighten accent text THAT SITS ON THE PAGE", and text
 * inside a chip sits on the chip.
 *
 * Restated as one test I can apply next time: follow the text to its nearest opaque background. If
 * that background moved, the text moves with it. If it did not, the text stays exactly where it
 * was. */
.pc-dark .podc-pill--new, .pc-dark .podc-pill--grade { color: #5b3ec9; }
.pc-dark .podc-pill--seen, .pc-dark .podc-pill.is-on { color: #1f7a45; }
.pc-dark .podc-pill--count { color: #4a5560; }
.pc-dark .podc-act--grade .podc-act__ic { color: #1f7a45; }
.pc-dark .podc-icard__hint { color: #6b46e5; }
.pc-dark .podc-icard__face--b .podc-icard__hint { color: #7a5a2e; }
.pc-dark .podc-icard__side { color: #b0674f; }
.pc-dark .podc-icard__face--b .podc-icard__side { color: #6b46e5; }
/* THE FACES THAT STAYED LIGHT NEED DARK TEXT — and ONLY those faces.
   I first wrote this as `.podc-card, .podc-card__f, .podc-card__b`, which darkened the flashcard
   FRONT too. The front's own background is var(--bg), so in dark it is #14151a and I had just put
   #14151a text on it: 1.00:1, perfectly invisible. Only the BACK is a light face. Same test as
   above — follow the text to its nearest opaque background and ask whether that background moved. */
.pc-dark .podc-card__b { color: #14151a; }
.pc-dark .podc-icard__face { color: #14151a; }
/* A light amber folder tile, which the sweep correctly kept: its glyph has to be dark. */
.pc-dark .podc-folder__ic { color: #7a5a2e; }
/* 4.21:1 against its own cream face — over the line by a hair, so it comes down a shade. */
.pc-dark .podc-icard__side { color: #8d4f3c; }
