:root{
  --ink:#36454F;            /* charcoal — headings, body, primary */
  --slate:#8D99AE;          /* secondary text, dividers */
  --paper:#F8F9FA;          /* alabaster — page background */
  --band:#EDF0F3;           /* subtle alternating section */
  --rule:#DFE3E8;
  --marker:#E76F51;         /* warm amber — accent, CTAs */
  --highlight:#FBDDD3;      /* soft amber wash for highlighted words */
  --pine:#2F3B44;           /* deep charcoal panels */
  --card:#FFFFFF;
  --rule-hover:#C3CAD3;     /* (was self-referential — fixed) */
  --shadow:54,69,79;
  --nav-bg:rgba(248,249,250,.92);
  --hl-tint:rgba(54,69,79,.07);   /* nav sliding-highlight pill */
  --hl-text:#2b2420;             /* text on the highlight-word swatch, fixed regardless of theme since the swatch itself doesn't invert */
  --paper-rgb:248,249,250;       /* rgb triple of --paper, for gradients that need alpha */
  --marker-rgb:231,111,81;       /* rgb triple of --marker, same purpose */
  --always-light:#F8F9FA;        /* fixed light text, for surfaces that stay dark in both themes (e.g. the solid-button hover circle) */
  --label:#5F6B78;               /* secondary text on a white card — --slate is only 2.88:1 there, this is 5.44:1 */
  --display:'Bricolage Grotesque',system-ui,sans-serif;
  --body:'Inter',system-ui,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,monospace;
  --gut:clamp(20px,5vw,64px);
  /* Shared by every blur-out-up reveal (see the block further down). Kept
     here rather than next to those rules because the first use is the
     session note, which sits well above them in the file. */
  --blur-ease:cubic-bezier(.22,1,.36,1);
  --blur-dur:.56s;
  /* One duration and one curve for the whole nav morph. These live here
     rather than on .nav because the promo bar has to travel on exactly the
     same timing — it slides up as the bar collapses into its pill, and two
     independently-declared curves would drift apart the moment either is
     tuned. .nav inherits them, so every var(--nav-dur) inside it is
     unchanged. */
  --nav-dur:1.15s;
  --nav-ease:cubic-bezier(.38,.06,.16,1);
  /* Promo bar. --promo-h is the switch: 0 means no bar and no space
     reserved for one, and html.promo-on (set before paint) is the only
     thing that lifts it. */
  --promo-h:0px;
  --promo-bg:#2F3B44;
  --promo-fg:#F8F9FA;
  /* --marker on pine is only 3.5:1. This is the same hue lightened to
     5.3:1, which the offer itself needs since that's the phrase people
     are meant to read. */
  --promo-accent:#F2A78E;
}



/* ---------- dark theme ---------- */
[data-theme="dark"]{
  --ink:#E7EAEC;
  --slate:#93A0AC;
  --paper:#14181B;
  --band:#1A1F23;
  --rule:#282F34;
  --marker:#E9835F;
  --pine:#1D2327;
  --card:#1B2125;
  --rule-hover:#3A4247;
  --shadow:0,0,0;
  --nav-bg:rgba(20,24,27,.92);
  --label:#93A0AC;
  --hl-tint:rgba(255,255,255,.07);
  --paper-rgb:20,24,27;
  --marker-rgb:233,131,95;
  /* Pine and paper are only a few points apart in dark mode, so the bar
     is lifted rather than reused — otherwise the strip vanishes into the
     page behind it. */
  --promo-bg:#222A2F;
  --promo-fg:#E7EAEC;
  --promo-accent:#E9835F;
}
html{color-scheme:light}
[data-theme="dark"]{color-scheme:dark}
*{box-sizing:border-box}
/* Matches the scrolled nav height (58px) so an anchored section sits
   flush under the bar, with no sliver of the previous section showing. */
html{scroll-behavior:smooth;scroll-padding-top:59px}
/* overflow-x:hidden removed sitewide — it forces overflow-y:auto per
   CSS spec (a hard browser rule, not fixable with an explicit declaration),
   which breaks position:sticky throughout the page. The actual overflow
   sources (nav flex-shrink, pricing toggle) are fixed at their source
   instead of papered over here. */
body{
  margin:0;background:var(--paper);color:var(--ink);
  font-family:var(--body);font-size:17px;line-height:1.6;
  -webkit-font-smoothing:antialiased;
}
img{max-width:100%}
a{color:inherit}
:focus-visible{outline:2.5px solid var(--marker);outline-offset:3px;border-radius:2px}
.wrap{max-width:1180px;margin:0 auto;padding:0 var(--gut)}
.eyebrow{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--slate);margin:0 0 18px;
}
h1,h2,h3{font-family:var(--display);font-weight:800;letter-spacing:-.022em;line-height:1.04;margin:0}
h2{font-size:clamp(30px,4.4vw,50px)}
h3{font-size:20px;letter-spacing:-.01em;line-height:1.25}
p{margin:0 0 16px}
.lede{font-size:clamp(17px,1.9vw,20px);color:var(--slate);max-width:56ch;line-height:1.55}

/* ---------- nav ---------- */
/* Fixed, not sticky: the bar shrinks on scroll, and if it sat in normal
   flow that height change would shove every element below it upward. */
.nav{position:fixed;top:0;left:0;right:0;z-index:60;background:var(--nav-bg);backdrop-filter:blur(10px);border-bottom:1px solid var(--rule)}
/* 70px is the unscrolled nav; --promo-h is 0 unless the promo bar is live,
   in which case it stacks on top. The transition only ever runs when the
   bar is dismissed mid-session — on load the value is already settled by
   the head script, before paint. */
body{padding-top:calc(70px + var(--promo-h));transition:padding-top .45s cubic-bezier(.22,1,.36,1)}
.nav-in{display:flex;align-items:center;gap:28px;height:var(--navh);transition:height .4s cubic-bezier(.22,1,.36,1)}
.nav{--navh:70px;transition:box-shadow .38s ease,background .38s ease}
.nav.scrolled{--navh:58px;box-shadow:0 12px 30px -24px rgba(var(--shadow),.55)}
.scroll-progress{position:absolute;left:0;bottom:-1px;height:2px;width:0;background:var(--marker);transition:width .08s linear;z-index:2}
.mark{display:flex;align-items:center;gap:9px;text-decoration:none;font-family:var(--display);font-weight:800;font-size:19px;letter-spacing:-.02em;flex:none}
.mark-x{display:grid;place-items:center;width:34px;height:34px;color:var(--ink);flex:none}
.mark-x svg{width:32px;height:32px}
.foot .mark-x{width:30px;height:30px}
.foot .mark-x svg{width:28px;height:28px}
.nav-links{position:relative;display:flex;gap:24px;margin-left:auto;align-items:center}
.nav-links a{font-size:14.5px;font-weight:500;text-decoration:none;color:var(--ink);opacity:.68;white-space:nowrap;transition:opacity .3s ease}
.nav-links a:hover{opacity:1}
.nav-cta{
  display:inline-flex;align-items:center;flex-shrink:0;
  font-family:var(--body);font-size:14.5px;font-weight:700;letter-spacing:-.01em;
  background:var(--ink);color:var(--paper);border:0;border-radius:100px;
  padding:12px 22px;cursor:pointer;text-decoration:none;white-space:nowrap;
}
.nav-cta:hover{background:var(--marker)}

.nav-toggle{
  display:none;flex-direction:column;justify-content:center;gap:5px;
  width:38px;height:38px;background:none;border:0;cursor:pointer;padding:0;flex:none;
}
.nav-toggle span{width:22px;height:2px;background:var(--ink);display:block;transition:.3s ease;transform-origin:center}
.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg)}
.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}

@media(max-width:600px){
  .nav-in{gap:6px}
  .mark{font-size:17px}
  .mark svg{width:28px!important;height:28px!important}
  /* Matching .btn-flow.nav-cta's specificity here isn't enough: at equal
     specificity a plain source-order tie goes to whichever rule is LATER
     in the file, and the unconditional .btn-flow.nav-cta padding (flow
     buttons section, further down) sits after this block — so it kept
     winning even after the classes matched. Scoping to .nav-in adds a
     third class, which beats that rule outright regardless of where
     either one sits in the file. */
  .nav-in .btn-flow.nav-cta{padding-left:12px;padding-right:14px}
}

@media(max-width:400px){
  /* Same source-order trap as above: .theme-btn{display:grid} (theme
     toggle section) is defined after this block, so an equal-specificity
     .theme-sw{display:none} here lost the tie. Scoping to .nav-in makes
     this rule strictly more specific so it wins unconditionally. */
  .nav-in .theme-sw{display:none}
}
/* Short label from here down. Checked by sweeping every width from 320 to
   600px in both English and Chinese and comparing the CTA button's right
   edge against the viewport — not eyeballed at a couple of sample sizes,
   which is how the old 390px cutoff still overflowed as high as ~450px
   despite three "verified empirically" breakpoints layered on top of it. */
.lbl-short{display:none}
@media(max-width:460px){
  .lbl-full{display:none}
  .lbl-short{display:inline}
  .nav-cta .flow-arrow.is-right{display:none}
}
@media(max-width:340px){
  .nav-in{gap:4px;padding:0 12px}
  .mark{font-size:0}
  .mark .mark-x,.mark svg{font-size:initial}
}
@media(max-width:980px){
  .nav-toggle{display:flex}
  .nav-links{
    display:none;position:absolute;top:var(--navh);left:0;right:0;
    flex-direction:column;align-items:stretch;gap:0;margin-left:0;
    background:var(--paper);border-bottom:1px solid var(--rule);
    padding:8px var(--gut) 16px;box-shadow:0 18px 30px -20px rgba(var(--shadow),.25);
  }
  .nav-links.open{display:flex}
  .nav-links a{padding:13px 0;border-bottom:1px solid var(--rule);font-size:15.5px}
  .nav-links a:last-child{border-bottom:0}
}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex;align-items:center;gap:10px;font-family:var(--body);
  font-size:16px;font-weight:500;padding:15px 26px;border-radius:100px;
  text-decoration:none;cursor:pointer;border:1px solid transparent;transition:.26s;
}
.btn-solid{background:var(--ink);color:var(--paper)}
.btn-solid:hover{background:var(--marker)}
.btn-ghost{border-color:var(--ink);color:var(--ink)}
.btn-ghost:hover{background:var(--ink);color:var(--paper)}
.btn-light{background:var(--highlight);color:var(--hl-text)}
.btn-light:hover{background:var(--card)}

/* ---------- hero ---------- */
/* ---------- logo strip (schools + partners) ---------- */
.logos-strip{padding:clamp(36px,5vw,56px) 0;border-bottom:1px solid var(--rule)}
.logos-block{display:flex;align-items:center;gap:clamp(16px,3vw,32px);padding:10px 0}
.logos-block + .logos-block{border-top:1px solid var(--rule)}
.logos-lab{
  flex:none;width:150px;font-family:var(--mono);font-size:11px;letter-spacing:.06em;
  color:var(--slate);line-height:1.4;
}
.logo-marquee{
  flex:1;min-width:0;overflow:hidden;padding:6px 0;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
}
.logo-track{
  display:flex;align-items:center;gap:clamp(12px,1.6vw,20px);width:max-content;
  animation:logoScroll 48s linear infinite;
}
@media(hover:hover){.logo-marquee:hover .logo-track{animation-play-state:paused}}
/* translateX(-50%) assumes the track holds exactly two copies of the set.
   Adding or removing a logo means updating BOTH halves or the loop jumps. */
@keyframes logoScroll{to{transform:translateX(-50%)}}
/* No plate. The white tiles this replaces were legible but read as eight
   bright blocks punched into the dark page. The logos now sit directly on
   the background, reversed to light in dark mode. */
.logo-tile{
  flex:none;display:grid;place-items:center;height:72px;padding:0 20px;
  transition:transform .28s cubic-bezier(.22,1,.36,1),opacity .28s ease;
}
.logo-tile img{
  display:block;height:50px;width:auto;max-width:160px;object-fit:contain;
  transition:filter .28s ease;
}
/* Measured against the dark page (#14181B), the ink in six of these eight
   sits between 1.0:1 and 1.4:1 — Oxford's navy is literally 1.0, i.e.
   invisible on its own. Rather than desaturating every mark to fix that
   (which threw away the actual brand colours), each tile gets a small
   light plate behind it instead: original colours are kept, and the plate
   supplies the contrast the dark page can't. */
[data-theme="dark"] .logo-tile{
  background:#555;border-radius:8px;padding:8px 16px;
}
[data-theme="dark"] .logo-tile img{filter:none}
/* TMU and PBAA are already solid-colour panels/badges, not line art — they
   read fine on their own plate like everything else, no special-casing
   needed any more. */

/* Badge-shaped marks are near square, so at the wordmark height they come
   out around 55px wide and read as an afterthought next to a 160px logotype.
   The class goes on the image, not the row: a future partner with a normal
   horizontal logo should still size like every other wordmark. */
.logo-tile img.logo-badge{height:64px}
@media(max-width:640px){.logo-tile img.logo-badge{height:52px}}

/* One partner is not a marquee. The track needs enough logos to fill the row
   twice before scrolling reads as a list in motion — below that it is one
   logo sliding past on a loop, which looks like a bug rather than a roster.
   So the row parks until there are enough. To turn it back into a marquee:
   drop .is-solo and repeat the full set in both halves of the track, the way
   the schools row above does. */
.logos-block.is-solo .logo-marquee{
  overflow:visible;-webkit-mask-image:none;mask-image:none;
}
.logos-block.is-solo .logo-track{animation:none;width:auto}
/* The schools row is inset by its own fade mask; without one, a static row
   of several logos needs the leading tile flush or it sits oddly far from
   its label. With only one logo (the current PBAA case), that flush-left
   rule instead left the badge stranded in the left corner of a wide empty
   row, reading as off-center. Center it and give it its normal padding
   back; the flush-left rule still applies automatically once a second
   logo joins the row. */
.logos-block.is-solo .logo-tile:first-child:not(:only-child){padding-left:0}
.logos-block.is-solo .logo-track:has(.logo-tile:only-child){justify-content:center}
@media(hover:hover){
  .logo-tile:hover{transform:translateY(-2px)}
}
@media(prefers-reduced-motion:reduce){
  .logo-track{animation:none}
  .logo-marquee{overflow-x:auto;-webkit-mask-image:none;mask-image:none}
  .logo-tile{transition:none}
}
@media(max-width:640px){
  .logos-block{flex-direction:column;align-items:flex-start;gap:12px}
  .logos-lab{width:auto}
  /* flex:1;min-width:0 only constrains width on the row axis — once the
     block stacks into a column, that trick stops working and the wide
     scrolling track was bursting out to its full content width instead
     of staying clipped to the viewport. Pin it explicitly instead. */
  .logo-marquee{width:100%}
  .logo-tile{height:58px;padding:0 14px}
  .logo-tile img{height:40px;max-width:128px}
}

.hero{
  position:relative;padding:clamp(48px,7vw,90px) 0 clamp(56px,7vw,86px);
  background-image:linear-gradient(var(--rule) 1px,transparent 1px),linear-gradient(90deg,var(--rule) 1px,transparent 1px);
  background-size:34px 34px;background-position:-1px -1px;
}
.hero::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(var(--paper-rgb),.55),var(--paper) 88%);pointer-events:none}
.hero-in{position:relative;z-index:2;display:grid;grid-template-columns:1.08fr .92fr;gap:clamp(32px,5vw,68px);align-items:center}
@media(max-width:900px){.hero-in{grid-template-columns:1fr;gap:44px}}
.hero h1{font-size:clamp(38px,6.1vw,70px)}
.hero .lede{margin-top:22px}
.hero-btns{display:flex;flex-wrap:wrap;gap:12px;margin-top:30px}
.hero-trust{display:flex;flex-wrap:wrap;gap:8px 20px;margin-top:34px;font-family:var(--mono);font-size:11.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--slate)}
.hero-trust span{display:flex;align-items:center;gap:7px}
.hero-trust span::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--marker)}
.hl{background:linear-gradient(transparent 2%,var(--highlight) 2% 96%,transparent 96%);padding:0 2px;color:var(--hl-text)}
/* Reject-word treatment: a crossed-out phrase in a heading, paired with .hl
   marking the phrase we're endorsing in the same sentence (see #ai h2). The
   strike sits in the accent colour so it reads as an active "no," not a
   faded-out one — the surrounding text stays full-weight and legible. */
.hl-no{text-decoration:line-through;text-decoration-color:var(--marker);text-decoration-thickness:3px;text-decoration-skip-ink:none}

/* ---------- session note (hero graphic) ---------- */
.snote{
  background:var(--card);border:1px solid var(--rule);border-radius:6px;
  padding:22px 24px 20px;box-shadow:0 18px 44px -30px rgba(var(--shadow),.4);
}
.snote-head{
  display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--slate);border-bottom:1px solid var(--rule);padding-bottom:12px;margin-bottom:14px;
}
.snote-course{font-family:var(--mono);font-weight:600;font-size:15px;letter-spacing:.01em;color:var(--ink);margin:0 0 16px}
.snote-block{margin-bottom:14px}
.snote-block:last-of-type{margin-bottom:0}
.snote-block p{margin:5px 0 0;font-size:14.5px;line-height:1.5;color:var(--ink)}
.snote-tag{
  display:inline-block;font-family:var(--mono);font-size:10px;letter-spacing:.07em;text-transform:uppercase;
  color:var(--slate);background:var(--band);border-radius:3px;padding:3px 8px;
}
.snote-tag-flag{color:var(--marker);background:rgba(231,111,81,.12)}
.snote-foot{
  margin-top:16px;padding-top:14px;border-top:1px solid var(--rule);
  font-size:12.5px;line-height:1.5;color:var(--slate);
}
/* Blur-out-up inside the session note. These children were already part of
   the hero entrance (.hero-in > div > *), which rises 22px over 1.1s — read
   as one block sliding up. Overridden here so the card frame holds still and
   the note writes itself into it line by line, which is what the graphic is
   actually claiming: a summary that arrives after the session.
   Needs .hero-in in the selector to outweigh the hero rule's `div` type
   selector, and the delays restart the stagger after the hero copy (.36s). */
.has-js .hero-in .snote > *{
  opacity:0;transform:translateY(10px);filter:blur(6px);
  transition:opacity var(--blur-dur) var(--blur-ease),
             transform var(--blur-dur) var(--blur-ease),
             filter var(--blur-dur) var(--blur-ease);
}
.hero-ready .hero-in .snote > *{opacity:1;transform:none;filter:blur(0)}
.has-js .hero-in .snote > *:nth-child(1){transition-delay:.26s}
.has-js .hero-in .snote > *:nth-child(2){transition-delay:.34s}
.has-js .hero-in .snote > *:nth-child(3){transition-delay:.42s}
.has-js .hero-in .snote > *:nth-child(4){transition-delay:.50s}
.has-js .hero-in .snote > *:nth-child(5){transition-delay:.58s}
.has-js .hero-in .snote > *:nth-child(6){transition-delay:.66s}
.has-js .hero-in .snote > *:nth-child(7){transition-delay:.74s}
.rack-foot i{display:inline-block;width:16px;height:9px;border:1.9px solid var(--marker);border-radius:50%;flex:none}

/* ---------- generic section ---------- */
.sec{padding:clamp(64px,8vw,110px) 0}
.sec-band{background:var(--band);border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
.sec-head{max-width:60ch;margin-bottom:clamp(36px,5vw,58px)}
.sec-head p{margin-top:18px}

/* ---------- thesis ---------- */
.three{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(24px,3.4vw,44px)}
@media(max-width:820px){.three{grid-template-columns:1fr;gap:34px}}
.card-t{border-top:2px solid var(--ink);padding-top:20px}
.card-t .tag{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--marker);display:block;margin-bottom:12px}
.card-t p{color:var(--slate);font-size:16px;margin:12px 0 0}

/* ---------- tutors ---------- */
/* align-items:start is load-bearing: grid rows are shared, so a closed card
   would otherwise stretch to the height of an open one beside it and show a
   large void under its own content. */
.tutors{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;align-items:start}
@media(max-width:640px){
  .tutors{grid-template-columns:1fr;gap:12px}
  /* The card is a <summary> the whole width of the screen, so the tap target
     needs to be comfortable and the contents must not crowd. */
  details.tutor{padding:16px}
  .tutor-top{gap:12px}
  .tutor-top h3{font-size:17px}
  .tutor-role{font-size:9.5px;letter-spacing:.06em}
  /* Course chips wrap rather than overflow: a tutor with eight codes was
     pushing the card wider than the viewport. */
  .chips{flex-wrap:wrap;gap:5px}
  .chip{font-size:10px;padding:3px 6px}
  .tutor-mode{font-size:9.5px;padding:4px 8px}
  /* Filters stack full width instead of squeezing four controls onto one
     row, where each ends up too narrow to read its own label. */
  .tutor-filters{flex-direction:column;gap:8px}
  .tutor-filters input,.tutor-filters select,.tutor-filters .xdd{width:100%}
}
.tutor-filters{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}
.tutor-filters input,.tutor-filters select{
  flex:1 1 200px;height:52px;padding:0 16px;border:1px solid var(--rule);border-radius:8px;
  font-family:var(--body);font-size:15px;background:var(--card);color:var(--ink);
  transition:border-color .3s ease,box-shadow .3s ease;
}
.tutor-filters input:hover,.tutor-filters select:hover{border-color:var(--rule-hover)}
.tutor-filters input:focus,.tutor-filters select:focus{
  border-color:var(--ink);box-shadow:0 0 0 3px rgba(var(--shadow),.07);
}
/* Keep the visible focus ring for keyboard users; drop it for mouse clicks */
.tutor-filters input:focus:not(:focus-visible),
.tutor-filters select:focus:not(:focus-visible){outline:none}
.tutor-filters input::placeholder{color:var(--slate)}
/* Search field: magnifier tucked inside on the left */
.tutor-filters input{
  padding-left:44px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235A6C78' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.5-3.5'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:left 15px center;background-size:17px;
}
/* Selects: replace the OS arrow with our own chevron */
.tutor-filters select{
  -webkit-appearance:none;-moz-appearance:none;appearance:none;
  padding-right:42px;cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235A6C78' stroke-width='2.2' 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;background-size:16px;
}
.tutor-filters select:hover,.tutor-filters select:focus{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23C4362B' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}
.tutor-filters select::-ms-expand{display:none}
@media(max-width:640px){.tutor-filters input,.tutor-filters select{flex-basis:100%}}
.tutor-count{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--slate);margin:0 0 16px}
.tutor-empty{font-size:15px;color:var(--slate);padding:32px 0;text-align:center}
.tutors-more-wrap{display:flex;justify-content:center;margin-top:22px}
.tutors-more-wrap:has(#tutors-more[hidden]){display:none}
/* The label swaps between "Show all 16 tutors" and "Show fewer tutors", which
   are different widths — without a floor the button resized and, being
   centre-aligned, slid sideways on every click. Wide enough for either, and
   for the shorter Chinese strings, which simply centre inside it. */
#tutors-more .flow-txt{display:inline-block;min-width:142px;text-align:center}
/* Chrome re-anchors the scroll position when content above the viewport
   changes height. That is usually welcome, but here it fires mid-animation
   against a grid the script is already resizing deliberately, and the two
   corrections fought: the page teleported ~500px and the button jumped
   upwards before travelling down. The script owns this resize, so opt the
   section out. */
#tutors,#tutors-grid,.tutors-more-wrap{overflow-anchor:none}
details.tutor{cursor:default}
details.tutor summary{cursor:pointer;list-style:none;position:relative;padding-right:26px}
details.tutor summary::-webkit-details-marker{display:none}
details.tutor summary::after{
  content:"+";position:absolute;right:0;top:-2px;font-family:var(--mono);
  font-size:20px;color:var(--marker);font-weight:400;
}
details.tutor[open]:not(.dp-ready) summary::after{content:"–"}
details.tutor p.bio{font-size:14.5px;color:var(--slate);line-height:1.5;margin:14px 0 0}
.tutor{background:var(--card);border:1px solid var(--rule);border-radius:8px;padding:20px 20px 18px;display:flex;flex-direction:column;gap:12px;box-shadow:0 10px 26px -20px rgba(var(--shadow),.5);transition:border-color .26s ease,transform .26s cubic-bezier(.22,1,.36,1),box-shadow .38s ease,padding .38s cubic-bezier(.22,1,.36,1),opacity .28s ease}
/* z-index anchor for the hover enlarge below: a scaled card has to sit over its
   neighbours in the grid, not under the next one's shadow. */
.tutor{position:relative;z-index:0}
@media(hover:hover){
  /* Hover magnifies the whole card rather than only tinting its border, so the
     one you're pointing at is the one that's easiest to read. Click still owns
     the real expand (this is a <details>/<summary>) — this is visibility only.
     scale() rather than a padding/size change so nothing in the grid reflows
     under the pointer, and the whole subtree (avatar, name, badges) comes up
     with it for free. */
  details.tutor:hover{
    border-color:var(--rule-hover);border-left-color:var(--marker);
    transform:translateY(-4px) scale(1.035);z-index:3;
    box-shadow:0 30px 56px -26px rgba(var(--shadow),.6);
  }
  [data-theme="dark"] details.tutor:hover{
    box-shadow:0 0 0 1px rgba(255,255,255,.08),0 30px 56px -26px rgba(0,0,0,.85);
  }
  /* An expanded card is already enlarged by .dp-open (bigger padding, avatar
     and type). Stacking the full hover scale on top of that pushes it into its
     neighbours, so it gets the lift and a much smaller bump instead. */
  details.tutor.dp-open:hover{transform:translateY(-4px) scale(1.012)}
}
/* Fallback for pointers that can't hover: keep the original quiet lift. */
@media(hover:none){
  .tutor:hover{border-color:var(--rule-hover);transform:translateY(-2px);box-shadow:0 20px 40px -24px rgba(var(--shadow),.55)}
}
/* Pagination reveal. The cards are toggled with [hidden] (display:none),
   which no transition can animate across — so the batch is animated with
   keyframes instead, staggered so it reads as one wave rather than six
   cards popping in at once. The classes are stripped on animationend:
   an animation with `both` fill would otherwise keep its final transform
   latched and silently kill the :hover translateY above. */
.tutor.tutor-in{animation:tutorIn .46s cubic-bezier(.22,1,.36,1) both}
.tutor.tutor-out{animation:tutorOut .26s cubic-bezier(.4,0,1,1) both}
/* Both are transform/opacity only — nothing here affects layout, so a card
   still settling after the grid has finished resizing cannot shift anything
   around it. The durations are deliberately shorter than the grid's own
   resize so the whole wave lands inside that window instead of trailing
   past it, which is what made the old reveal read as two separate events. */
@keyframes tutorIn{
  from{opacity:0;transform:translateY(14px) scale(.985)}
  to{opacity:1;transform:none}
}
@keyframes tutorOut{
  from{opacity:1;transform:none}
  to{opacity:0;transform:translateY(-8px) scale(.985)}
}
@media(prefers-reduced-motion:reduce){
  .tutor.tutor-in,.tutor.tutor-out{animation:none}
}
/* Dark mode: a black shadow barely reads against an already-dark page, so
   cards need a different cue to separate from it — a faint light edge
   instead of relying on shadow depth. */
[data-theme="dark"] .tutor{box-shadow:0 0 0 1px rgba(255,255,255,.045),0 10px 26px -20px rgba(0,0,0,.6)}
[data-theme="dark"] .tutor:hover{box-shadow:0 0 0 1px rgba(255,255,255,.09),0 20px 40px -24px rgba(0,0,0,.7)}
/* Expanded: the card grows in place rather than taking over the full row.
   Full-width expansion pushed whichever card sat beside it down to the next
   row, leaving a visible hole — and with two open in the same row, two
   holes. Growing in place means any number can be open at once and nothing
   else on the grid moves. */
details.tutor.dp-open{padding:24px 24px 22px}
details.tutor.dp-open .tutor-top{gap:16px}
details.tutor.dp-open .avatar{width:58px;height:58px;font-size:19px;border-radius:9px;transition:width .3s ease,height .3s ease,font-size .3s ease}
details.tutor.dp-open h3{font-size:19px}
details.tutor.dp-open .tutor-role{font-size:11px}
details.tutor.dp-open .bio{font-size:15.5px;line-height:1.65}
/* Fun fact used to open a 260px side column next to the bio — on a short
   fact that left a tall, mostly-empty box floating beside the text. It now
   renders as a single inline row under the bio at every width instead (see
   the shared p.fun-fact rule below), so the box only ever takes the height
   its text needs. */
.tutor-top{display:flex;align-items:center;gap:12px}
.avatar{width:46px;height:46px;border-radius:5px;display:grid;place-items:center;font-family:var(--mono);font-weight:600;font-size:15px;color:var(--hl-text);background:var(--highlight);flex:none;overflow:hidden}
/* A tutor with a photo loses the initials tint, otherwise it shows as a
   coloured edge behind the image while it loads. */
.avatar.has-photo{background:var(--band)}
.avatar img{width:100%;height:100%;object-fit:cover;display:block;border-radius:inherit}
.tutor h3{font-size:16.5px}
.tutor-role{font-family:var(--mono);font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:var(--slate);margin-top:3px;line-height:1.4}
.tutor p{font-size:14.5px;color:var(--slate);margin:0;line-height:1.5}
.chips{display:flex;flex-wrap:wrap;gap:5px;margin-top:auto;padding-top:4px}
.chip{font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:.03em;background:var(--band);border-radius:3px;padding:4px 7px}
/* Mode badge: visually distinct outline style (vs. filled course chips) so
   it reads as "how", not "what subject". Only rendered when a tutor has
   actually supplied this via the onboarding form — omit entirely otherwise. */
.tutor-mode{
  display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:10px;
  letter-spacing:.04em;text-transform:uppercase;color:var(--slate);
  border:1px solid var(--rule);border-radius:100px;padding:4px 9px;margin-top:10px;
  align-self:flex-start;transition:border-color .26s ease,color .26s ease;
}
details.tutor:hover .tutor-mode{border-color:var(--rule-hover);color:var(--ink)}
.tutor-mode svg{width:10px;height:10px;flex:none;fill:none;stroke:currentColor;stroke-width:2}
/* In-person carries a travel fee, so it gets the accent — a parent scanning
   the grid should be able to see which tutors that applies to. */
.tutor-mode.is-travel{border-color:var(--highlight);color:var(--marker)}
[data-theme="dark"] .tutor-mode.is-travel{border-color:rgba(233,131,95,.4)}

/* Two badges now share the row, so they wrap as a group rather than the
   mode pill sitting alone with its own margin. */
.tutor-badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;align-items:center}
.tutor-badges .tutor-mode{margin-top:0}
/* Screening status. Same pill as the mode badge so it reads as one more
   fact about the tutor rather than a warning label bolted onto the card —
   the point is to inform a parent, not to mark anyone as suspect. */
.tutor-vss{
  display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:10px;
  letter-spacing:.04em;text-transform:uppercase;color:var(--slate);
  border:1px solid var(--rule);border-radius:100px;padding:4px 9px;
  transition:border-color .26s ease,color .26s ease;
}
.tutor-vss svg{width:10px;height:10px;flex:none;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
details.tutor:hover .tutor-vss{border-color:var(--rule-hover);color:var(--ink)}
/* Pine rather than a green tick: a check on file is a baseline expectation
   being met, not an award, and a green badge on some cards would push the
   others toward reading as a red flag. */
.tutor-vss.is-cleared{border-color:rgba(54,69,79,.28);color:var(--ink)}
[data-theme="dark"] .tutor-vss.is-cleared{border-color:rgba(255,255,255,.26)}
/* Fun fact: lives in the expanded state only, secondary to the bio so it
   doesn't compete for attention on first read. */
details.tutor p.fun-fact{font-size:13.5px;color:var(--slate);line-height:1.5;margin:8px 0 0;font-style:italic}
details.tutor p.fun-fact b{font-style:normal;font-weight:600;color:var(--ink)}

/* ---------- course table ---------- */
.cols2{display:grid;grid-template-columns:1fr 1fr;gap:clamp(28px,4vw,60px)}
@media(max-width:760px){.cols2{grid-template-columns:1fr;gap:40px}}
.catalog{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:clamp(20px,2.6vw,32px)}
.cat-note{margin-top:clamp(28px,3.4vw,40px);padding-top:22px;border-top:1px solid var(--rule);font-size:15.5px;color:var(--slate);max-width:62ch}
.col-h{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--marker);padding-bottom:12px;border-bottom:2px solid var(--ink);margin-bottom:12px}
.chip-row{display:flex;flex-wrap:wrap;gap:7px}
.ccode{
  display:inline-block;font-family:var(--mono);font-weight:600;font-size:12.5px;letter-spacing:.02em;
  background:var(--band);border:1px solid var(--rule);border-radius:5px;padding:6px 9px;
  color:var(--ink);cursor:help;transition:border-color .26s ease,background .26s ease,transform .3s cubic-bezier(.22,1,.36,1),color .26s ease;
}
@media(hover:hover){.ccode:hover{border-color:var(--rule-hover);background:var(--card)}}
.ccode.is-active{border-color:var(--marker);background:var(--card)}
.ccode:focus-visible{outline:2.5px solid var(--marker);outline-offset:2px}

/* Course detail popover. Appended to <body> rather than next to the chip:
   .cat-block carries a transform for its reveal animation, and a transformed
   ancestor becomes the containing block for position:fixed, which would peg
   the card to the column instead of the viewport. */
.ccode-pop{
  position:fixed;z-index:80;width:290px;max-width:calc(100vw - 24px);
  background:var(--card);border:1px solid var(--rule);border-radius:10px;
  padding:14px 16px;box-shadow:0 18px 44px -14px rgba(var(--shadow),.3);
  opacity:0;pointer-events:none;
  /* Grows out of the chip rather than sliding in from nowhere: it starts
     slightly small, with its origin set to the edge nearest the chip, so the
     motion reads as the code expanding into its full title. */
  transform:scale(.94) translateY(4px);
  transform-origin:var(--pop-ox,50%) var(--pop-oy,100%);
  transition:opacity .3s ease,transform .46s cubic-bezier(.22,1,.36,1);
}
.ccode-pop.is-open{opacity:1;transform:none}
.ccode-pop-code{font-family:var(--mono);font-size:10.5px;letter-spacing:.11em;color:var(--marker);margin:0 0 7px}
.ccode-pop-name{font-family:var(--display);font-weight:800;font-size:15.5px;line-height:1.3;letter-spacing:-.01em;color:var(--ink);margin:0 0 9px}
.ccode-pop-desc{font-size:13.5px;line-height:1.6;color:var(--label);margin:0}
/* Content resolves a beat behind the card, so the card arrives first and the
   title and description settle into it. */
.ccode-pop-code,.ccode-pop-name,.ccode-pop-desc{
  opacity:0;transform:translateY(6px);
  transition:opacity .38s ease,transform .48s cubic-bezier(.22,1,.36,1);
}
.ccode-pop.is-open .ccode-pop-code{opacity:1;transform:none;transition-delay:.06s}
.ccode-pop.is-open .ccode-pop-name{opacity:1;transform:none;transition-delay:.12s}
.ccode-pop.is-open .ccode-pop-desc{opacity:1;transform:none;transition-delay:.18s}
/* The chip lifts and takes the accent while its card is open, so the link
   between the two is obvious when several sit side by side. */
.ccode.is-active{transform:translateY(-2px);color:var(--marker)}
@media(prefers-reduced-motion:reduce){
  .ccode-pop,.ccode-pop-code,.ccode-pop-name,.ccode-pop-desc,.ccode{transition:none}
  .ccode.is-active{transform:none}
}

/* ---------- how it works: sticky step tracker ---------- */
.sticky-wrap{display:grid;grid-template-columns:.95fr 1.05fr;gap:clamp(30px,5vw,64px)}
@media(max-width:820px){.sticky-wrap{grid-template-columns:1fr;gap:8px}}
.sticky-side{position:sticky;top:108px;align-self:start}
.sticky-head h2{font-size:clamp(27px,3.6vw,42px)}
.sticky-track{position:relative;padding-left:26px;margin-top:clamp(26px,3.4vw,40px)}

.sticky-rail{position:absolute;left:0;top:8px;bottom:8px;width:2px;background:var(--rule);border-radius:2px;overflow:hidden}
.sticky-rail i{
  display:block;border-radius:2px;background:var(--marker);
  width:100%;height:var(--fill,0%);
  transition:height 1.1s cubic-bezier(.22,1,.36,1),width 1.1s cubic-bezier(.22,1,.36,1);
}
.sticky-side ol{list-style:none;padding:0;margin:0;counter-reset:s}
.sticky-side li{
  counter-increment:s;font-family:var(--mono);font-size:12px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--slate);padding:13px 0 13px 42px;position:relative;
  transition:color .8s ease,opacity .8s ease,transform .9s cubic-bezier(.22,1,.36,1);
  opacity:.45;
}
.sticky-side li::before{content:"0" counter(s);position:absolute;left:0;color:var(--rule);transition:color .5s ease}
.sticky-side li.on{color:var(--ink);opacity:1;transform:translateX(4px)}
.sticky-side li.on::before{color:var(--marker)}

.sticky-step{
  min-height:clamp(340px,58vh,480px);display:flex;flex-direction:column;justify-content:center;
  padding:26px 0;border-bottom:1px solid var(--rule);
}
.sticky-step:first-child{padding-top:0}
.sticky-step:last-child{border-bottom:0;padding-bottom:0}
.sticky-step h3{font-size:clamp(22px,2.9vw,30px);margin-bottom:13px}
.sticky-step p{color:var(--slate);font-size:16.5px;margin:0;max-width:50ch}
/* Inactive steps recede so the current one reads as the focus */
.has-js .sticky-step{
  opacity:.28;transform:translateY(16px);
  transition:opacity 1.3s cubic-bezier(.22,1,.36,1),transform 1.3s cubic-bezier(.22,1,.36,1);
}
.has-js .sticky-step.active{opacity:1;transform:translateY(0)}

#sticky-steps{counter-reset:st}
.sticky-step{counter-increment:st}
@media(max-width:820px){
  /* Same behaviour as desktop, laid out for a narrow screen: the heading
     and tracker pin to the top and the tracker runs horizontally. */
  .sticky-side{
    position:sticky;top:58px;z-index:5;
    background:var(--paper);padding:14px 0 16px;
    border-bottom:1px solid var(--rule);
  }
  .sticky-head .eyebrow{display:none}
  .sticky-head h2{font-size:clamp(21px,6vw,26px)}
  .sticky-track{display:block;padding-left:0;margin-top:14px;padding-bottom:12px}
  .sticky-step:first-child{padding-top:26px}
  .sticky-rail{left:0;right:0;top:auto;bottom:0;width:auto;height:2px}
  .sticky-rail i{height:100%;width:var(--fill,0%)}
  .sticky-side ol{display:flex;gap:16px}
  .sticky-side li{padding:0 0 8px;font-size:10.5px;letter-spacing:.09em}
  .sticky-side li::before{position:static;display:inline;margin-right:5px}
  .sticky-side li.on{transform:none}
  .sticky-step{min-height:clamp(280px,58vh,400px);padding:24px 0}
  .sticky-step h3::before{content:none}
}

/* ---------- testimonials: auto-scrolling, data-driven ---------- */
/* Columns and content are generated by JS from the TESTIMONIALS array near
   the bottom of the page — add an entry there and it appears here with no
   markup changes. Column count grows automatically as the list grows. */
.tmarquee-wrap{
  position:relative;max-height:600px;overflow:hidden;
  -webkit-mask-image:linear-gradient(to bottom,transparent,#000 8%,#000 92%,transparent);
  mask-image:linear-gradient(to bottom,transparent,#000 8%,#000 92%,transparent);
}
.tmarquee{display:flex;justify-content:center;gap:16px}
/* flex:1 1 0 rather than a fixed width. Three 380px columns plus gaps come
   to 1172px, which does not fit the 1180px wrap once its gutters are taken
   off (~1052px at desktop), so fixed columns overflowed and .tmarquee-wrap's
   overflow:hidden clipped the outer ones — the left card lost its first few
   characters on every line. Letting them share the row keeps every card
   whole at any width; max-width stops them stretching when there are only
   one or two. */
.tcol{
  display:flex;flex-direction:column;gap:16px;
  flex:1 1 0;min-width:0;max-width:380px;
  animation:tScroll linear infinite;animation-duration:var(--tdur,26s);
}
.tcol.is-reverse{animation-direction:reverse}
@media(hover:hover){.tcol:hover{animation-play-state:paused}}
@keyframes tScroll{from{transform:translateY(0)}to{transform:translateY(-50%)}}
.tcard{
  background:var(--card);border:1px solid var(--rule);border-left:3px solid var(--marker);
  border-radius:8px;padding:22px 24px;
  transition:border-color .26s ease,box-shadow .26s ease;
}
/* Dark mode: these cards sit on a --band section, and in the dark palette
   --card (#1B2125) and --band (#1A1F23) are within two points of each other
   — 1.02:1, so the card surface was effectively invisible and the quotes
   read as loose text on the page. This lifts the card to roughly the same
   separation the light theme has (1.16 vs 1.14), rather than inventing a
   brighter panel that would glare. */
[data-theme="dark"] .tcard{
  background:#242B30;
  border-color:#374149;
  border-left-color:var(--marker);
  box-shadow:0 0 0 1px rgba(255,255,255,.035),0 16px 34px -26px rgba(0,0,0,.85);
}
@media(hover:hover){
  [data-theme="dark"] .tcard:hover{border-color:#44505A;box-shadow:0 0 0 1px rgba(255,255,255,.07),0 20px 40px -26px rgba(0,0,0,.9)}
  .tcard:hover{border-color:var(--rule-hover)}
  .tcard:hover{border-left-color:var(--marker)}
}
.tcard p{font-size:15.5px;line-height:1.62;color:var(--ink);margin:0 0 18px}
.tcard footer{display:flex;align-items:center;gap:11px}
.tavatar{
  width:38px;height:38px;border-radius:50%;flex:none;display:grid;place-items:center;
  background:var(--pine);color:var(--always-light);font-family:var(--mono);font-weight:600;font-size:12.5px;
}
/* --pine is a near-black in the dark palette, so the initials disc vanished
   against the card at 1.02:1. In light mode it's a dark disc on white at
   11.5:1 — this restores comparable presence by going lighter instead. */
[data-theme="dark"] .tavatar{background:#39444C;color:#F8F9FA}
.tcard cite{font-style:normal;font-weight:600;font-size:14px;color:var(--ink);display:block}
.tcard .trole{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--slate)}
@media(prefers-reduced-motion:reduce){.tcol{animation:none}}
/* No-JS fallback list — hidden the moment JS runs and replaces it */
.tfallback{display:grid;gap:16px;max-width:640px;margin:0 auto}

/* ---------- pricing ---------- */
.price-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:26px}
/* ---------- comparison (what makes us different) ----------
   The section carries the same aurora backdrop as the founder's note and the
   closing CTA — the page's existing signal for "this is an argument, not a
   label". Content sits above it. */
.compare-sec{position:relative;overflow:hidden}
.compare-sec > .wrap{position:relative;z-index:1}
.compare-list{display:flex;flex-direction:column;gap:56px}
/* Three named groups — transparency, quality, consistency — so the rows read as
   evidence for the three values the company states everywhere else, rather than
   as a loose list of gripes. The pillar head is the heavier of the two labels;
   the mono .compare-topic inside each row stays the quiet sub-label it was. */
.compare-group{display:flex;flex-direction:column;gap:26px}
.compare-pillar{display:flex;align-items:baseline;gap:8px 16px;flex-wrap:wrap;padding-bottom:15px;border-bottom:1px solid var(--rule)}
.compare-pillar-n{font-family:var(--mono);font-size:11px;letter-spacing:.12em;color:var(--marker);flex:none}
.compare-pillar h3{margin:0;font-size:23px;letter-spacing:-.015em;line-height:1.15;flex:none}
.compare-pillar p{margin:0;flex:1 1 280px;font-size:14px;line-height:1.55;color:var(--slate)}
@media(max-width:700px){.compare-pillar h3{font-size:20px}.compare-pillar p{flex-basis:100%}}
.compare-topic{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);margin:0 0 12px;display:flex;align-items:center;gap:12px}
/* Hairline running off the label to the right edge, so each row reads as its
   own band without needing a heavier divider. */
.compare-topic::after{content:"";flex:1;height:1px;background:var(--rule)}
.compare-pair{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:700px){.compare-pair{grid-template-columns:1fr}}
/* top:0 is the anchor for the hover lift. It rides `top` rather than transform
   so it cannot fight the reveal's translate — the same split .ref-card uses. */
.compare-cell{border-radius:8px;padding:24px 26px;position:relative;top:0}
.compare-cell p:last-child{margin:0;font-size:15px;line-height:1.6;color:var(--ink)}
.compare-tag{font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;margin:0 0 10px;display:flex;align-items:center;gap:9px}
.compare-tag i{font-style:normal;font-weight:600;font-size:12px;width:20px;height:20px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex:none;line-height:1}
.compare-them{background:var(--band);border:1px solid var(--rule)}
.compare-them .compare-tag{color:var(--slate)}
.compare-them .compare-tag i{background:var(--rule);color:var(--slate)}
.compare-them p:last-child{color:var(--slate)}
/* A warm wash off the top edge, so this side reads as raised rather than
   merely differently-bordered. It carries most of the contrast in dark mode,
   where --band and --card sit about a point apart and the border was doing
   the separating on its own. Static fill — nothing here interpolates. */
.compare-us{
  background-color:var(--card);
  background-image:linear-gradient(180deg,rgba(var(--marker-rgb),.07),rgba(var(--marker-rgb),0) 46%);
  border:1px solid var(--rule);border-left:3px solid var(--marker);
  box-shadow:0 18px 40px -32px rgba(var(--shadow),.5);
}
[data-theme="dark"] .compare-us{
  background-image:linear-gradient(180deg,rgba(var(--marker-rgb),.13),rgba(var(--marker-rgb),0) 46%);
  box-shadow:0 0 0 1px rgba(255,255,255,.05),0 18px 40px -32px rgba(0,0,0,.6);
}
.compare-us .compare-tag{color:var(--marker)}
.compare-us .compare-tag i{background:var(--marker);color:#fff}
/* Only the X Tutoring side answers the cursor. The other one sitting inert is
   the point. */
@media(hover:hover){
  .compare-us:hover{
    border-color:var(--ink);border-left-color:var(--marker);top:-4px;
    box-shadow:0 30px 54px -34px rgba(var(--shadow),.55);
  }
  [data-theme="dark"] .compare-us:hover{box-shadow:0 0 0 1px rgba(255,255,255,.1),0 30px 54px -34px rgba(0,0,0,.7)}
  .compare-them:hover{border-color:var(--rule-hover)}
}
/* ---------- how we teach (approach) ----------
   Three movements: the shape of the hour, how tutors are prepared for it, and
   the reading it comes from. Everything rides the page's own tokens so it
   inverts with the theme — the draft this replaced carried its own palette and
   an rgba(255,255,255,.45) card wash that went invisible in dark mode. */
.ap-sub{font-size:20px;letter-spacing:-.012em;margin:54px 0 8px}
.ap-note{font-size:15.5px;line-height:1.65;color:var(--ink);max-width:62ch;margin:0 0 8px}
.ap-note.is-quiet{color:var(--label)}
/* The hour is a sequence, so it's drawn as one. The card grid this replaced
   read as seven unrelated features and left a ragged 4+3 row; a single rail
   runs in the order the hour actually does, which is the claim the section
   is making in the first place. */
.ap-steps{list-style:none;padding:0;margin:26px 0 0;counter-reset:ap;position:relative;max-width:66ch}
/* The rail fades out at both ends so it reads as a span of time rather than
   an arrow pointing off the section. It sits under the nodes, which carry an
   opaque fill so the line breaks cleanly behind each one. */
.ap-steps::before{
  content:"";position:absolute;left:15px;top:10px;bottom:16px;width:2px;
  background:linear-gradient(180deg,transparent,var(--rule) 7%,var(--rule) 93%,transparent);
}
.ap-steps li{counter-increment:ap;position:relative;display:grid;grid-template-columns:32px 1fr;gap:20px;padding-bottom:24px}
.ap-steps li:last-child{padding-bottom:0}
.ap-steps li::before{
  content:counter(ap,decimal-leading-zero);grid-column:1;
  display:grid;place-items:center;width:32px;height:32px;border-radius:50%;
  background:var(--card);border:1px solid var(--rule);color:var(--marker);
  font-family:var(--mono);font-size:10px;line-height:1;z-index:1;
  transition:background .3s ease,border-color .3s ease,color .3s ease;
}
@media(hover:hover){
  .ap-steps li:hover::before{background:var(--marker);border-color:var(--marker);color:var(--always-light)}
}
.ap-step-b{padding-top:4px;min-width:0}
.ap-steps strong{display:block;font-size:15.5px;margin-bottom:3px}
.ap-steps span{display:block;font-size:14.5px;line-height:1.6;color:var(--label)}
/* The note home lands after the hour is over, so it takes a filled node and a
   label instead of a number. It closes the loop to the parent, who is the
   person actually reading this page. */
.ap-steps li.ap-after{padding-top:6px;padding-bottom:0}
.ap-steps li.ap-after::before{content:"\2192";background:var(--marker);border-color:var(--marker);color:var(--always-light);font-size:13px}
.ap-step-k{font-family:var(--mono);font-size:10px;letter-spacing:.11em;text-transform:uppercase;color:var(--marker);display:block;margin-bottom:4px}
/* Cascade on scroll, matching the stagger the category blocks use. */
.has-js .ap-steps li.reveal:nth-child(2){transition-delay:.05s}
.has-js .ap-steps li.reveal:nth-child(3){transition-delay:.1s}
.has-js .ap-steps li.reveal:nth-child(4){transition-delay:.15s}
.has-js .ap-steps li.reveal:nth-child(5){transition-delay:.2s}
.has-js .ap-steps li.reveal:nth-child(6){transition-delay:.25s}
.has-js .ap-steps li.reveal:nth-child(7){transition-delay:.3s}
.has-js .ap-steps li.reveal:nth-child(8){transition-delay:.35s}
/* Same hairline-separated rhythm as the comparison rows, so the page's two
   long argument sections read as one document rather than two pasted blocks. */
.ap-train{margin:22px 0 0}
.ap-train > div{display:grid;grid-template-columns:200px 1fr;gap:10px 28px;padding:20px 0;border-top:1px solid var(--rule)}
.ap-train > div:last-child{border-bottom:1px solid var(--rule)}
.ap-train dt{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--slate);padding-top:3px}
.ap-train dd{margin:0;font-size:15px;line-height:1.62;color:var(--ink);max-width:62ch}
@media(max-width:700px){.ap-train > div{grid-template-columns:1fr;gap:7px}}
/* Sources sit closed. The claim is that they exist and can be checked, not
   that a parent has to read six papers to believe the section above. */
.ap-sources{margin-top:46px;border-top:1px solid var(--rule);padding-top:22px}
.ap-sources summary{
  cursor:pointer;font-family:var(--mono);font-size:11px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--slate);list-style:none;display:flex;
  align-items:center;gap:9px;transition:color .2s ease;
}
.ap-sources summary::-webkit-details-marker{display:none}
.ap-sources summary::after{content:"+";font-size:14px;color:var(--marker)}
.ap-sources[open] summary::after{content:"\2013"}
@media(hover:hover){.ap-sources summary:hover{color:var(--ink)}}
.ap-sources ol{margin:16px 0 0;padding-left:20px;display:flex;flex-direction:column;gap:10px}
.ap-sources li{font-size:14px;line-height:1.55;color:var(--label)}
.ap-sources a{color:var(--ink);text-decoration:underline;text-decoration-color:var(--rule-hover);text-underline-offset:3px;transition:color .2s ease,text-decoration-color .2s ease}
@media(hover:hover){.ap-sources a:hover{color:var(--marker);text-decoration-color:var(--marker)}}
/* ---------- referrals ---------- */
.ref-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:12px}
@media(max-width:700px){.ref-grid{grid-template-columns:1fr}}
.ref-card{
  background:var(--card);border:1px solid var(--rule);border-left:3px solid var(--marker);
  border-radius:8px;padding:30px 32px;
  position:relative;top:0;
  transition:border-color .8s ease,box-shadow .8s ease,top .8s cubic-bezier(.22,1,.36,1);
  -webkit-tap-highlight-color:transparent;
}
[data-theme="dark"] .ref-card{box-shadow:0 0 0 1px rgba(255,255,255,.05),0 10px 26px -20px rgba(0,0,0,.6)}
[data-theme="dark"] .ref-card:hover{box-shadow:0 0 0 1px rgba(255,255,255,.1),0 22px 44px -30px rgba(0,0,0,.7)}
@media(hover:hover){
  .ref-card:hover{
    border-color:var(--ink);border-left-color:var(--marker);top:-6px;
    box-shadow:0 26px 50px -32px rgba(var(--shadow),.55);
  }
  .ref-card:hover .ref-who{color:var(--ink)}
}
.ref-card:active{
  border-color:var(--ink);border-left-color:var(--marker);top:-1px;
  box-shadow:0 8px 18px -14px rgba(var(--shadow),.5);
  transition-duration:.13s;
}
.ref-card:active .ref-who{color:var(--ink)}
.ref-fig{
  font-family:var(--display);font-weight:800;font-size:44px;letter-spacing:-.03em;line-height:1;
  color:var(--ink);margin:2px 0 10px;display:flex;align-items:baseline;gap:9px;
}
.ref-fig sub{font-family:var(--mono);font-weight:400;font-size:12.5px;letter-spacing:.02em;color:var(--slate);bottom:0}
.ref-who{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--slate);margin:0 0 10px;transition:color .5s ease}
.ref-reward{font-size:15.5px;font-weight:600;color:var(--ink);margin:0 0 16px;line-height:1.4}
.ref-reward span{display:block;font-size:14px;font-weight:400;color:var(--slate);margin-top:3px}
.ref-note{font-family:var(--mono);font-size:11.5px;color:var(--slate);margin:0;padding-top:16px;border-top:1px solid var(--rule)}
@media(max-width:820px){.price-grid{grid-template-columns:1fr}}
.price{
  background:var(--card);border:1px solid var(--rule);border-radius:8px;padding:26px;
  display:flex;flex-direction:column;position:relative;top:0;
  transition:border-color .55s ease,box-shadow .55s ease,top .55s cubic-bezier(.22,1,.36,1);
  -webkit-tap-highlight-color:transparent;
}
/* Dark mode: same issue as the tutor cards — a dark shadow is invisible
   against an already-dark page, and the card fill is too close in tone to
   the page behind it (measured ~1.1:1, essentially indistinguishable).
   A faint light edge does the separation work instead. */
[data-theme="dark"] .price{box-shadow:0 0 0 1px rgba(255,255,255,.05),0 10px 26px -20px rgba(0,0,0,.6)}
[data-theme="dark"] .price:hover{box-shadow:0 0 0 1px rgba(255,255,255,.1),0 22px 44px -30px rgba(0,0,0,.7)}
/* Hover only where hovering actually exists, so touch devices don't get
   stuck in a hover state after a tap. */
@media(hover:hover){
  .price:hover{
    border-color:var(--ink);top:-5px;
    box-shadow:0 22px 44px -30px rgba(var(--shadow),.55);
  }
  .price:hover .price-lab{color:var(--ink)}
  .price:hover li::before{background:var(--marker);border-color:var(--marker)}
}
/* Pressed: the card sinks slightly and the shadow tightens */
.price:active{
  border-color:var(--ink);top:-1px;
  box-shadow:0 8px 18px -14px rgba(var(--shadow),.5);
  transition-duration:.13s;
}
.price:active .price-lab{color:var(--ink)}
.price:active li::before{background:var(--marker);border-color:var(--marker)}
.price-lab,.price li::before{transition:color .5s ease,background .5s ease,border-color .5s ease}
.price-lab{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);margin-bottom:14px}
.price-fig{font-family:var(--display);font-weight:800;font-size:44px;letter-spacing:-.03em;line-height:1}
.price-fig sub{font-family:var(--mono);font-size:12px;font-weight:400;color:var(--slate);letter-spacing:.06em;vertical-align:baseline;margin-left:5px}
.price ul{list-style:none;padding:0;margin:18px 0 0;display:grid;gap:8px}
.price li{font-size:14.5px;color:var(--slate);padding-left:18px;position:relative}
.price li::before{content:"";position:absolute;left:0;top:9px;width:7px;height:7px;border-radius:50%;background:var(--highlight);border:1px solid var(--ink)}
.price-note{font-size:14.5px;color:var(--slate);max-width:60ch}

/* ---------- faq ---------- */
.faq{max-width:1180px}
.faq-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:0 36px}
@media(max-width:980px){.faq-cols{grid-template-columns:1fr 1fr}}
@media(max-width:700px){.faq-cols{grid-template-columns:1fr}}
.faq details{border-bottom:1px solid var(--rule)}
.faq summary{
  cursor:pointer;list-style:none;padding:22px 40px 22px 0;position:relative;
  font-family:var(--display);font-weight:600;font-size:19px;letter-spacing:-.01em;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"+";position:absolute;right:6px;top:19px;font-family:var(--mono);font-size:24px;color:var(--marker);font-weight:400}
.faq details[open]:not(.dp-ready) summary::after{content:"–"}
.faq-body{padding:0 0 24px;color:var(--slate);font-size:16px;max-width:70ch}
.faq-body p:last-child{margin:0}

/* ---------- cta band ---------- */
.cta{background:var(--pine);color:var(--paper);padding:clamp(60px,8vw,100px) 0}
.cta h2{color:#fff;max-width:20ch}
.cta p{color:rgba(248,249,250,.7);margin-top:20px;max-width:52ch;font-size:18px}
.cta .btn-light{margin-top:30px}
.cta-fine{font-family:var(--mono);font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:rgba(248,249,250,.45);margin-top:24px}

/* ---------- footer ---------- */
.foot{border-top:1px solid var(--rule);padding:40px 0;font-size:14px;color:var(--slate)}
.foot-in{display:flex;flex-wrap:wrap;gap:18px 32px;align-items:center}
.foot a{text-decoration:none}
.foot a:hover{color:var(--ink)}
.foot-r{margin-left:auto;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
@media(max-width:640px){.foot-r{margin-left:0}}

/* ---------- recruit view ---------- */
.recruit{background:var(--pine);color:var(--paper);min-height:100vh;padding:clamp(50px,7vw,84px) 0 clamp(60px,8vw,100px)}
.recruit h1{color:#fff;font-size:clamp(34px,5.4vw,60px);max-width:16ch}
.recruit h2{color:#fff}
.recruit .lede{color:rgba(248,249,250,.72)}
.recruit .eyebrow{color:var(--highlight)}
.back{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:rgba(248,249,250,.6);text-decoration:none;margin-bottom:30px;background:none;border:0;cursor:pointer;padding:0}
.back:hover{color:var(--highlight)}
.r-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(20px,3vw,32px);margin-top:clamp(40px,6vw,64px)}
@media(max-width:760px){.r-grid{grid-template-columns:1fr}}
.r-card{border:1px solid rgba(248,249,250,.16);border-radius:6px;padding:26px}
.r-card h3{color:#fff;margin-bottom:10px}
.r-card p{color:rgba(248,249,250,.68);font-size:15.5px;margin:0}
.r-card .tag{font-family:var(--mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--highlight);display:block;margin-bottom:12px}
.paytable{margin-top:18px;border-top:1px solid rgba(248,249,250,.16)}
.paytable div{display:flex;justify-content:space-between;align-items:baseline;gap:14px;padding:9px 0;border-bottom:1px solid rgba(248,249,250,.1)}
.paytable span{font-size:14px;color:rgba(248,249,250,.6)}
.paytable b{font-family:var(--mono);font-weight:600;font-size:14px;color:var(--highlight);white-space:nowrap}
.r-form{background:rgba(248,249,250,.05);border:1px solid rgba(248,249,250,.16);border-radius:6px;padding:clamp(24px,3.4vw,40px);margin-top:clamp(40px,6vw,64px);max-width:660px}
.r-form label{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:rgba(248,249,250,.6);margin:18px 0 7px}
.r-form input,.r-form select,.r-form textarea{
  width:100%;background:rgba(248,249,250,.07);border:1px solid rgba(248,249,250,.2);
  border-radius:4px;padding:12px 13px;color:var(--paper);font-family:var(--body);font-size:15px;
}
.r-form input::placeholder,.r-form textarea::placeholder{color:rgba(248,249,250,.32)}
.r-form select option{color:var(--ink)}
.r-form .btn{margin-top:26px}
.r-note{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:rgba(248,249,250,.4);margin-top:16px}
[hidden]{display:none!important}

/* ---------- preloader ---------- */
/* The fade-out is driven by a CSS animation, NOT by JS. If JavaScript
   fails to load or errors, the overlay still clears itself after 2.6s,
   so the site can never be permanently hidden behind it. JS just
   dismisses it sooner, as soon as the page is actually ready. */
#preloader{
  position:fixed;inset:0;z-index:500;background:var(--pine);
  display:grid;place-items:center;
  animation:preFail .55s ease 2.6s forwards;
}
@keyframes preFail{to{opacity:0;visibility:hidden}}
#preloader.done{
  animation:none;opacity:0;visibility:hidden;
  transition:opacity .5s ease,visibility 0s linear .5s;
}
.pre-inner{display:flex;flex-direction:column;align-items:center;gap:16px}
.pre-mark{display:grid;place-items:center;width:58px;height:58px;background:rgba(248,249,250,.06);border-radius:13px;color:var(--paper)}
.pre-mark svg{width:38px;height:38px}
.pre-word{font-family:var(--display);font-weight:800;font-size:19px;letter-spacing:-.02em;color:var(--paper)}
.pre-bar{display:block;width:132px;height:2px;background:rgba(248,249,250,.16);border-radius:2px;overflow:hidden}
.pre-bar i{display:block;height:100%;width:0;background:var(--marker);border-radius:2px;animation:preBar 2.2s cubic-bezier(.3,.8,.4,1) forwards}
@keyframes preBar{0%{width:0}55%{width:72%}100%{width:100%}}
#preloader.done .pre-bar i{width:100%;transition:width .34s ease}

/* Masked headline reveal: words rise into place behind a clipping mask.
   The parent block only fades (no translate) so the two don't double up. */
.has-js .sec-head.reveal,.has-js .sticky-head.reveal,
.has-js .story-head.reveal,.has-js .ai-head.reveal{transform:none}
.has-js .js-words .word{display:inline-block;overflow:hidden;vertical-align:bottom;padding-bottom:.14em;margin-bottom:-.14em}
.has-js .js-words .word > span{display:inline-block;transform:translateY(112%);transition:transform 1.05s cubic-bezier(.22,1,.36,1)}
.has-js .in-view .js-words .word > span{transform:translateY(0)}

/* ---------- blur-out-up ----------
   A second reveal vocabulary alongside the masked rise above. The mask is
   hard-edged and typographic, which suits a section announcing itself; this
   one is softer, and is used in the three places where the page is making an
   argument rather than labelling a block — the founder's note, the session-
   note example, and the AI position. Words arrive out of focus and settle.

   Two forms, same curve and duration so they read as one family:
     .js-words.js-blur  per word, for headings and short values
     .reveal.is-blur    whole block, for paragraphs and rows

   Travel and blur radius are in em, not px. At a fixed 6px a 46px headline
   barely seems to move while a 10px tag looks thrown; in em both drift by
   the same proportion of their own size. */
.has-js .js-words.js-blur .word{
  /* The mask rule above clips to the word box. A blur halo spills past that
     box by design, so clipping would shear the top and bottom off it. */
  overflow:visible;padding-bottom:0;margin-bottom:0;vertical-align:baseline;
}
.has-js .js-words.js-blur .word > span{
  opacity:0;transform:translateY(.34em);filter:blur(.15em);
  transition:opacity var(--blur-dur) var(--blur-ease),
             transform var(--blur-dur) var(--blur-ease),
             filter var(--blur-dur) var(--blur-ease);
}
.has-js .in-view .js-words.js-blur .word > span,
.has-js .js-words.js-blur.in-view .word > span{
  opacity:1;transform:none;filter:blur(0);
}

/* Whole-block variant. It rides the existing .reveal observer rather than
   adding a second one — the class only swaps the curve and adds the blur. */
.has-js .reveal.is-blur{
  opacity:0;transform:translateY(10px);filter:blur(6px);
  transition:opacity var(--blur-dur) var(--blur-ease),
             transform var(--blur-dur) var(--blur-ease),
             filter var(--blur-dur) var(--blur-ease);
}
.has-js .reveal.is-blur.in-view{opacity:1;transform:none;filter:blur(0)}

/* Red-pen wipe under nav links on hover */
.nav-links a{position:relative}
@media(min-width:981px){
  .nav-links a::after{
    content:"";position:absolute;left:0;right:0;bottom:-4px;height:1.5px;
    background:var(--marker);transform:scaleX(0);transform-origin:right;
    transition:transform .4s cubic-bezier(.22,1,.36,1);
  }
  .nav-links a:hover::after{transform:scaleX(1);transform-origin:left}
}

/* Course rows: gentle tint so the eye can track a row while scanning */
.cat-block{transition:background .3s ease}



/* The .reveal rule replaces the whole transition shorthand, which was
   silently killing the hover transitions on cards that also animate in.
   Re-declare both sets together for those elements. */
.has-js .price.reveal{
  transition:opacity .95s cubic-bezier(.22,1,.36,1),transform .95s cubic-bezier(.22,1,.36,1),
             border-color .55s ease,box-shadow .55s ease,top .55s cubic-bezier(.22,1,.36,1);
}
.has-js .ref-card.reveal{
  transition:opacity .95s cubic-bezier(.22,1,.36,1),transform .95s cubic-bezier(.22,1,.36,1),
             border-color .8s ease,box-shadow .8s ease,top .8s cubic-bezier(.22,1,.36,1);
}

/* Comparison cards. Same blur-out-up vocabulary as the founder's note, for
   the same reason: the section argues rather than labels. Reveal and hover
   are declared in one list per the note above, and the delays are given
   per-property — a single `transition-delay` here would also postpone the
   hover, which has to stay instant. Property order below is fixed by that:
   opacity, transform, filter, then border-color, box-shadow, top. */
.has-js .compare-cell.reveal{
  opacity:0;transform:translateY(12px);filter:blur(7px);
  transition:opacity var(--blur-dur) var(--blur-ease),
             transform var(--blur-dur) var(--blur-ease),
             filter var(--blur-dur) var(--blur-ease),
             border-color .55s ease,box-shadow .55s ease,
             top .55s cubic-bezier(.22,1,.36,1);
}
.has-js .compare-cell.reveal.in-view{opacity:1;transform:none;filter:blur(0)}
/* The claim lands, then the answer to it — a beat apart, not a cascade. */
.has-js .compare-them.reveal{transition-delay:.05s,.05s,.05s,0s,0s,0s}
.has-js .compare-us.reveal{transition-delay:.15s,.15s,.15s,0s,0s,0s}
/* The badges settle after their card, with the only overshoot in the section.
   Everything else eases out; these two are the beat you actually notice. */
.has-js .compare-cell .compare-tag i{
  opacity:0;transform:scale(.55);
  transition:opacity .3s ease,transform .5s cubic-bezier(.34,1.56,.64,1);
}
.has-js .compare-cell.in-view .compare-tag i{opacity:1;transform:scale(1)}
.has-js .compare-them.reveal .compare-tag i{transition-delay:.28s}
.has-js .compare-us.reveal .compare-tag i{transition-delay:.38s}



/* ---------- why we exist ---------- */
.story{
  background:var(--pine);color:var(--paper);
  border-radius:16px;position:relative;overflow:hidden;
  padding:clamp(48px,7vw,84px) clamp(28px,5vw,64px);
  margin:0 auto;max-width:1180px;
  box-shadow:0 50px 90px -45px rgba(var(--shadow),.6),0 0 0 1px rgba(248,249,250,.06);
}
.story > *:not(.aurora){position:relative;z-index:1}
.story .eyebrow{color:var(--highlight)}
.story-head::before{content:"";display:block;width:46px;height:3px;background:var(--marker);border-radius:2px;margin-bottom:20px}
.story-grid{position:relative;z-index:1;display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(32px,5vw,72px);align-items:start}
@media(max-width:860px){.story-grid{grid-template-columns:1fr;gap:36px}}
.story h2{color:#fff;font-size:clamp(28px,4vw,46px);max-width:18ch}
.story p{color:rgba(248,249,250,.74);font-size:17px;line-height:1.68;margin:22px 0 0;max-width:56ch}
.story p strong{color:#fff;font-weight:600}
.story-sign{
  display:flex;align-items:center;gap:11px;margin-top:30px;
  font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
  color:rgba(248,249,250,.55);
}
.story-sign::before{content:"";width:20px;height:2px;background:var(--marker);border-radius:2px;flex:none}
.receipt{
  background:rgba(248,249,250,.05);border:1px solid rgba(248,249,250,.16);
  border-radius:8px;padding:clamp(22px,2.6vw,30px);
}
.receipt-h{font-family:var(--mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--highlight);margin:0 0 18px}
.receipt-row{display:flex;justify-content:space-between;align-items:baseline;gap:16px;padding:13px 0;border-bottom:1px solid rgba(248,249,250,.1)}
.receipt-row:last-of-type{border-bottom:0}
.receipt-row span{font-size:15px;color:rgba(248,249,250,.66)}
.receipt-row b{font-family:var(--mono);font-weight:600;font-size:15px;color:#fff;white-space:nowrap}
.receipt-row.is-tutor b{color:var(--highlight)}
.receipt-note{font-size:14px;color:rgba(248,249,250,.5);margin:18px 0 0;line-height:1.6}

/* Blur-out-up, applied to the founder's note. The headline splits per word
   (see the JS), so the prose underneath follows it in sequence instead of
   landing with it — the paragraphs read as the argument settling after the
   claim, and the signature last. Delays start just past the headline's own
   word run (~13 words at 28ms). */
.has-js .story-head p,.has-js .story-head .story-sign{
  opacity:0;transform:translateY(10px);filter:blur(6px);
  transition:opacity var(--blur-dur) var(--blur-ease),
             transform var(--blur-dur) var(--blur-ease),
             filter var(--blur-dur) var(--blur-ease);
}
.has-js .story-head.in-view p,.has-js .story-head.in-view .story-sign{
  opacity:1;transform:none;filter:blur(0);
}
/* The eyebrow is a <p> too, so the prose is types 2 and 3, not 1 and 2 —
   numbering them 1 and 2 left the closing paragraph with no delay at all and
   it beat the one above it into place. It leads rather than waits: it labels
   what follows, so it has to be legible before the headline resolves. */
.has-js .story-head .eyebrow{transition-delay:.04s}
.has-js .story-head p:nth-of-type(2){transition-delay:.30s}
.has-js .story-head p:nth-of-type(3){transition-delay:.40s}
.has-js .story-head .story-sign{transition-delay:.50s}

/* The receipt card arrives whole and its rows resolve inside it, so the eye
   is walked down the split — what you pay, what the tutor gets, what we
   keep — rather than being handed all three at once. The card itself only
   fades; the rows carry the movement. */
.has-js .receipt.reveal{transform:none}
.has-js .receipt > *{
  opacity:0;transform:translateY(10px);filter:blur(6px);
  transition:opacity var(--blur-dur) var(--blur-ease),
             transform var(--blur-dur) var(--blur-ease),
             filter var(--blur-dur) var(--blur-ease);
}
.has-js .receipt.in-view > *{opacity:1;transform:none;filter:blur(0)}
.has-js .receipt > *:nth-child(1){transition-delay:.10s}
.has-js .receipt > *:nth-child(2){transition-delay:.18s}
.has-js .receipt > *:nth-child(3){transition-delay:.26s}
.has-js .receipt > *:nth-child(4){transition-delay:.34s}
.has-js .receipt > *:nth-child(5){transition-delay:.42s}
.has-js .receipt > *:nth-child(6){transition-delay:.50s}



/* Collapsible bodies clip during the open/close animation */
.faq-body,.tfaq-body,details.tutor .tutor-more{overflow:hidden}
/* Marker colour eases with the panel (rotation would be invisible on +/-) */
.faq summary::after,.tfaq summary::after,details.tutor summary::after{
  transition:color .34s ease;
}


/* ---------- smooth accordion (grid-row technique) ---------- */
/* Animating to "auto" height is what CSS can't do natively. The fix: the
   collapsible body becomes a single-row grid track that transitions between
   0fr and 1fr, with an inner wrapper clipping the overflow. No JS height
   measurement involved, so there's nothing to get out of sync. */
.dp-wrap{display:grid;grid-template-rows:0fr;transition:grid-template-rows .5s cubic-bezier(.22,1,.36,1);}
.dp-inner{overflow:hidden;min-height:0}
details.dp-open .dp-wrap{grid-template-rows:1fr}
/* Once JS has wrapped a panel (.dp-ready), the marker follows our own class
   instead of the native [open] attribute, which JS keeps permanently true. */
details.dp-ready summary::after{content:"+"}
details.dp-ready.dp-open summary::after{content:"–"}


/* ---------- pricing toggle ---------- */
.pt-row{display:flex;flex-wrap:wrap;align-items:center;gap:16px;margin-bottom:26px}
.pricing-toggle{
  position:relative;display:grid;grid-template-columns:repeat(4,1fr);
  background:var(--band);border:1px solid var(--rule);border-radius:100px;padding:4px;gap:2px;
}
.pt-thumb{
  position:absolute;top:0;left:0;
  background:var(--ink);border-radius:100px;
  transition:transform .5s cubic-bezier(.22,1,.36,1),width .5s cubic-bezier(.22,1,.36,1),height .5s cubic-bezier(.22,1,.36,1);
}
.pt-opt{
  position:relative;z-index:1;background:none;border:0;cursor:pointer;
  font-family:var(--body);font-size:13.5px;font-weight:600;color:var(--slate);
  padding:11px 14px;border-radius:100px;white-space:nowrap;
  transition:color .4s ease;
}
.pt-opt.is-on{color:var(--paper)}
.pt-hint{font-size:14.5px;color:var(--slate);margin:0}
.pf-num{display:inline-block;transition:opacity .3s ease,transform .3s ease}
.pf-num.is-swapping{opacity:0;transform:translateY(-3px)}
.pf-total{
  font-family:var(--mono);font-size:12px;color:var(--ink);height:16px;margin-top:4px;
  opacity:0;transform:translateY(-3px);
  transition:opacity .45s ease,transform .45s cubic-bezier(.22,1,.36,1);
}
.pf-total.is-shown{opacity:1;transform:none}
.pf-save{
  font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--marker);height:16px;margin-top:6px;
  opacity:0;transform:translateY(-3px);
  transition:opacity .4s ease,transform .4s cubic-bezier(.22,1,.36,1);
}
.pf-save.is-shown{opacity:1;transform:none}
@media(max-width:640px){
  .pricing-toggle{grid-template-columns:repeat(2,1fr);width:100%}
  .pt-opt{padding:11px 10px;font-size:13px}
}

/* ---------- group per-student table ---------- */
.price-group .group-table{display:flex;flex-direction:column;gap:2px;margin-bottom:4px}
.grow{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:10px;padding:9px 0;border-bottom:1px solid var(--rule)}
.grow:last-child{border-bottom:0}
.grow-n{font-size:13.5px;color:var(--slate)}
.grow-fig{display:flex;align-items:baseline;gap:4px;font-weight:700;font-size:17px;color:var(--ink)}
.grow-unit{font-family:var(--mono);font-size:10.5px;font-weight:400;color:var(--slate)}
.grow-note{font-family:var(--mono);font-size:9.5px;letter-spacing:.04em;text-transform:uppercase;color:var(--slate);text-align:right}
.price-group .pf-save{margin-top:2px}

/* ---------- package policies ---------- */
.pkg-policies{margin-top:clamp(28px,3.4vw,40px);padding-top:24px;border-top:1px solid var(--rule)}
.pkg-policies-h{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--slate);margin:0 0 14px}
.pkg-policies ul{list-style:none;margin:0;padding:0;display:grid;gap:11px;max-width:70ch}
.pkg-policies li{font-size:14.5px;line-height:1.55;color:var(--slate);padding-left:16px;position:relative}
.pkg-policies li::before{content:"";position:absolute;left:0;top:9px;width:6px;height:6px;border-radius:50%;background:var(--marker)}
.pkg-policies li b{color:var(--ink);font-weight:600}


/* ---------- aurora backdrop (dark sections only) ---------- */
/* Radial gradients rather than filter:blur() — visually near-identical but
   far cheaper to composite, which matters on phones. */
.aurora{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:0}
.aurora i{position:absolute;display:block;border-radius:50%;will-change:transform}
.aurora .a1{
  width:min(70vw,760px);aspect-ratio:1;top:-22%;left:-12%;
  background:radial-gradient(circle,rgba(231,111,81,.26),rgba(231,111,81,0) 68%);
  animation:auroraA 28s ease-in-out infinite alternate;
}
.aurora .a2{
  width:min(62vw,660px);aspect-ratio:1;bottom:-26%;right:-10%;
  background:radial-gradient(circle,rgba(141,153,174,.22),rgba(141,153,174,0) 68%);
  animation:auroraB 34s ease-in-out infinite alternate;
}
.aurora .a3{
  width:min(46vw,520px);aspect-ratio:1;top:32%;right:22%;
  background:radial-gradient(circle,rgba(231,111,81,.14),rgba(231,111,81,0) 70%);
  animation:auroraC 40s ease-in-out infinite alternate;
}
@keyframes auroraA{from{transform:translate3d(0,0,0) scale(1)}to{transform:translate3d(14%,10%,0) scale(1.18)}}
@keyframes auroraB{from{transform:translate3d(0,0,0) scale(1.05)}to{transform:translate3d(-16%,-12%,0) scale(.92)}}
@keyframes auroraC{from{transform:translate3d(0,0,0) scale(.95)}to{transform:translate3d(-12%,16%,0) scale(1.15)}}
/* Content must sit above the backdrop */
.cta,.recruit{position:relative;overflow:hidden}
.cta > .wrap,.recruit > .wrap{position:relative;z-index:1}
@media(prefers-reduced-motion:reduce){
  .aurora i{animation:none}
}



/* Hold each variant's background steady on hover — the expanding circle is
   the only thing that should reveal the fill colour. Without this, the old
   instant-hover rules jump to their end colour before the circle finishes
   growing (or, for .btn-light, jump to a different colour entirely), which
   either masks the animation or fights it outright. */
.btn-flow.btn-solid:hover{background:var(--ink);color:var(--always-light)}
.btn-flow.nav-cta:hover{background:var(--ink);color:var(--always-light)}
.btn-flow.btn-ghost:hover{background:transparent}
.btn-flow.btn-light:hover{background:var(--highlight)}

/* ---------- flow buttons: arrow flows through, fill expands from centre ---------- */
.btn-flow{position:relative;overflow:hidden;transition:border-radius .5s cubic-bezier(.23,1,.32,1),
  border-color .5s ease,color .4s ease,background .4s ease;}
.btn-flow:hover{border-radius:12px}
.flow-txt{position:relative;z-index:2;display:inline-block;
  transform:translateX(-3px);transition:transform .6s cubic-bezier(.19,1,.22,1);}
.btn-flow:hover .flow-txt{transform:translateX(15px)}
.flow-arrow{position:absolute;top:50%;width:16px;height:16px;margin-top:-8px;z-index:2;
  fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;
  transition:left .7s cubic-bezier(.34,1.56,.64,1),right .7s cubic-bezier(.34,1.56,.64,1);}
.flow-arrow.is-right{right:20px}
.flow-arrow.is-left{left:-30%}
.btn-flow:hover .flow-arrow.is-right{right:-30%}
.btn-flow:hover .flow-arrow.is-left{left:20px}
.flow-circle{position:absolute;z-index:1;top:50%;left:50%;width:16px;height:16px;
  margin:-8px 0 0 -8px;border-radius:50%;opacity:0;
  transition:width .7s cubic-bezier(.19,1,.22,1),height .7s cubic-bezier(.19,1,.22,1),
             margin .7s cubic-bezier(.19,1,.22,1),opacity .38s ease;}
.btn-flow:hover .flow-circle{width:900px;height:900px;margin:-450px 0 0 -450px;opacity:1}

/* Fill colour + text/arrow colour on hover, one rule per existing variant */
.btn-flow.btn-solid .flow-circle,.btn-flow.nav-cta .flow-circle{background:var(--pine)}
.btn-flow.btn-ghost,.btn-flow.btn-solid,.btn-flow.btn-light{padding-left:30px;padding-right:44px}
.btn-flow.btn-ghost .flow-circle{background:var(--ink)}
.btn-flow.btn-ghost:hover{color:var(--paper);border-color:transparent}
.btn-flow.btn-ghost-dark{padding-left:30px;padding-right:44px}
.btn-flow.btn-ghost-dark .flow-circle{background:var(--paper)}
.btn-flow.btn-ghost-dark:hover{color:var(--pine);border-color:transparent}
.btn-flow.btn-light .flow-circle{background:var(--hl-text)}
.btn-flow.btn-light:hover{color:var(--always-light)}
.btn-flow.nav-cta{padding-left:24px;padding-right:40px}
@media(prefers-reduced-motion:reduce){
  .btn-flow,.btn-flow *{transition:none!important}
  .flow-arrow.is-left{display:none}
  .btn-flow:hover .flow-circle{opacity:0;width:16px;height:16px}
}





/* ---------- cursor ring ---------- */
/* A replacement, not an addition: the native arrow is hidden and the dot is
   the pointer. Desktop pointers only — the whole thing is switched off for
   touch and for reduced motion below, where the native cursor stays. */
.cursor-dot{
  position:fixed;top:0;left:0;width:5px;height:5px;border-radius:50%;
  background:var(--marker);pointer-events:none;z-index:401;
  transform:translate(-50%,-50%);opacity:0;transition:opacity .38s ease;
}
.cursor-ring{
  position:fixed;top:0;left:0;width:28px;height:28px;border-radius:50%;
  border:1.25px solid var(--ink);pointer-events:none;z-index:400;
  transform:translate(-50%,-50%);opacity:0;
  transition:opacity .38s ease,width .38s cubic-bezier(.22,1,.36,1),height .38s cubic-bezier(.22,1,.36,1),
             border-color .34s ease,background .34s ease;
  will-change:transform;
}
.cursor-dot.is-on{opacity:1}
.cursor-ring.is-on{opacity:.32}
.cursor-ring.is-active{width:48px;height:48px;opacity:.6;border-color:var(--marker);background:rgba(231,111,81,.08)}
@media(hover:none),(pointer:coarse){.cursor-dot,.cursor-ring{display:none!important}}
@media(prefers-reduced-motion:reduce){.cursor-dot,.cursor-ring{display:none!important}}
/* The native cursor goes only once the custom one is actually on screen: JS
   adds .cursor-custom on the first pointermove, so a page that hasn't been
   moved over yet — or one where the effect bailed out — never ends up with no
   pointer at all. Text entry keeps its I-beam: a 5px dot is no substitute for
   knowing where the caret will land. */
body.cursor-custom,body.cursor-custom *{cursor:none}
body.cursor-custom input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=color]),
body.cursor-custom textarea,
body.cursor-custom [contenteditable]{cursor:text}


/* ---------- nav: sliding highlight + auth placeholders ---------- */
/* Desktop-only pill that glides beneath whichever main nav link is hovered
   or focused, positioned by JS. Doesn't cover Login/Sign up or the cross-
   page link, which stay as their own distinct elements. */
.nav-hl{
  position:absolute;top:6px;bottom:6px;left:0;width:0;border-radius:7px;
  background:var(--hl-tint);opacity:0;pointer-events:none;z-index:0;
  transition:transform .45s cubic-bezier(.22,1,.36,1),width .45s cubic-bezier(.22,1,.36,1),opacity .3s ease;
}
.nav-links a{position:relative;z-index:1}
@media(max-width:980px){.nav-hl{display:none}}

.nav-auth-link{margin-left:2px}
@media(max-width:980px){
  .nav-auth-link:first-of-type{margin-top:8px;border-top:1px solid var(--rule);padding-top:19px!important}
}


/* ---------- theme toggle ---------- */
.theme-btn{
  display:grid;place-items:center;width:36px;height:36px;flex:none;
  background:none;border:1px solid var(--rule);border-radius:100px;cursor:pointer;
  color:var(--ink);transition:border-color .34s ease,color .34s ease;margin-left:2px;
}
@media(hover:hover){.theme-btn:hover{border-color:var(--ink)}}
.theme-btn svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.9;
  stroke-linecap:round;stroke-linejoin:round}
.theme-btn .moon{display:none}
[data-theme="dark"] .theme-btn .sun{display:none}
[data-theme="dark"] .theme-btn .moon{display:block}


/* ---------- custom dropdown (progressive enhancement over <select>) ---------- */
.xdd{position:relative}
.xdd-native{position:absolute!important;opacity:0;width:1px;height:1px;overflow:hidden;pointer-events:none}
.xdd-btn{
  width:100%;height:52px;padding:0 16px;border:1px solid var(--rule);border-radius:8px;
  background:var(--card);color:var(--ink);font-family:var(--body);font-size:15px;
  display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;text-align:left;
  transition:border-color .3s ease,box-shadow .3s ease;
}
@media(hover:hover){.xdd-btn:hover{border-color:var(--rule-hover)}}
.xdd.is-open .xdd-btn{border-color:var(--ink);box-shadow:0 0 0 3px rgba(var(--shadow),.07)}
.xdd-btn:focus-visible{outline:2.5px solid var(--marker);outline-offset:2px}
.xdd-chev{width:16px;height:16px;flex:none;fill:none;stroke:var(--slate);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;transition:transform .34s ease,stroke .3s ease}
.xdd.is-open .xdd-chev{transform:rotate(180deg);stroke:var(--marker)}
.xdd-menu{
  position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:45;
  background:var(--card);border:1px solid var(--rule);border-radius:10px;padding:6px;
  box-shadow:0 20px 44px -20px rgba(var(--shadow),.4);
  opacity:0;transform:translateY(-6px);pointer-events:none;
  transition:opacity .28s ease,transform .28s cubic-bezier(.22,1,.36,1);
  max-height:280px;overflow-y:auto;
}
.xdd.is-open .xdd-menu{opacity:1;transform:translateY(0);pointer-events:auto}
.xdd-item{
  display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;
  padding:10px 11px;border-radius:6px;background:none;border:0;cursor:pointer;text-align:left;
  font-family:var(--body);font-size:14.5px;color:var(--ink);
}
@media(hover:hover){.xdd-item:hover{background:var(--band)}}
.xdd-item.is-selected{font-weight:600;color:var(--marker)}
.xdd-check{width:15px;height:15px;flex:none;fill:none;stroke:var(--marker);stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
@media(prefers-reduced-motion:reduce){.xdd-menu,.xdd-chev{transition:none}}

.xdd-panel .xdd-btn{
  height:auto;padding:13px 14px;border-radius:6px;
  background:rgba(248,249,250,.07);border:1px solid rgba(248,249,250,.2);color:var(--paper);
}
@media(hover:hover){.xdd-panel .xdd-btn:hover{border-color:rgba(248,249,250,.4)}}
.xdd-panel.is-open .xdd-btn{border-color:rgba(248,249,250,.5);box-shadow:none}
.xdd-panel .xdd-chev{stroke:rgba(248,249,250,.55)}
.xdd-panel.is-open .xdd-chev{stroke:var(--marker)}
.xdd-panel .xdd-menu{background:#212b26;border-color:rgba(248,249,250,.16)}
.xdd-panel .xdd-item{color:var(--paper)}
@media(hover:hover){.xdd-panel .xdd-item:hover{background:rgba(248,249,250,.08)}}
.xdd-panel .xdd-item.is-selected{color:var(--marker)}


/* ---------- footer social icons ---------- */
.social{display:flex;gap:2px;padding:5px;border-radius:14px;background:var(--band);border:1px solid var(--rule);flex:none}
.social a{
  position:relative;display:grid;place-items:center;width:38px;height:38px;border-radius:10px;
  color:var(--slate);text-decoration:none;transition:color .3s ease;
}
.social a svg{width:17px;height:17px;position:relative;z-index:2;transition:transform .34s cubic-bezier(.34,1.56,.64,1)}
.social a::before{
  content:"";position:absolute;inset:2px;border-radius:8px;background:var(--card);
  opacity:0;transform:scale(.88);transition:opacity .34s ease,transform .34s cubic-bezier(.34,1.56,.64,1);
}
.social a::after{
  content:"";position:absolute;bottom:3px;left:50%;translate:-50% 0;width:0;height:2px;border-radius:2px;
  background:var(--marker);opacity:0;transition:width .34s ease,opacity .34s ease;
}
.social a .tip{
  position:absolute;bottom:calc(100% + 9px);left:50%;translate:-50% 4px;
  padding:5px 9px;border-radius:7px;background:var(--pine);color:var(--always-light);
  font-family:var(--mono);font-size:10.5px;letter-spacing:.03em;white-space:nowrap;
  opacity:0;pointer-events:none;transition:opacity .3s ease,translate .3s ease;
}
.social a .tip::after{
  content:"";position:absolute;top:100%;left:50%;translate:-50% 0;
  width:6px;height:6px;background:var(--pine);rotate:45deg;margin-top:-3px;
}
@media(hover:hover){
  .social a:hover{color:var(--ink)}
  .social a:hover svg{transform:scale(1.08)}
  .social a:hover::before{opacity:1;transform:scale(1)}
  .social a:hover::after{width:12px;opacity:1}
  .social a:hover .tip{opacity:1;translate:-50% 0}
}

/* ---------- cross-page transitions ---------- */
/* Native view transitions where supported; browsers without it just
   navigate normally, so this is purely additive. */
@view-transition{navigation:auto}
@keyframes xtFadeOut{to{opacity:0}}
@keyframes xtFadeIn{from{opacity:0}}
@media(prefers-reduced-motion:no-preference){
  ::view-transition-old(root){animation:xtFadeOut .26s ease both}
  ::view-transition-new(root){animation:xtFadeIn .34s ease both}
}
/* Preloader is a first-impression device, not something to sit through on
   every internal click. After the first page of a session it's skipped. */
.skip-pre #preloader{display:none!important}

@media(prefers-reduced-motion:reduce){
  *{animation-duration:.001s!important;transition-duration:.001s!important}
  html{scroll-behavior:auto}
  .ring path{stroke-dashoffset:0}
  .reveal{opacity:1!important;transform:none!important}
  .js-words .word > span{transform:none!important}
  /* The rules above already release opacity and transform. Blur is the one
     property this reveal adds, and it has to be cleared separately or the
     text stays permanently out of focus for anyone who asked for less
     motion — the worst possible outcome of a motion preference. */
  .js-words.js-blur .word > span,.reveal.is-blur,
  .hero-in .snote > *,.receipt > *,.story-head p,
  .story-head .story-sign,.ai-col li,
  .compare-cell,.compare-cell .compare-tag i{
    opacity:1!important;transform:none!important;
    filter:none!important;transition-delay:0s!important;
  }
  .sticky-step{opacity:1!important;transform:none!important}
  .sticky-side li{opacity:1!important;transform:none!important}
  .hero-in > div > *,.hero-in .rack{opacity:1!important;transform:none!important}
  .nav-in{transition:none}
  .nav,.promo,body{transition:none}
  #preloader{animation:preFail .01s ease .2s forwards}
  .pre-bar i{animation:none;width:100%}
}

.has-js .reveal{opacity:0;transform:translateY(20px);transition:opacity .95s cubic-bezier(.22,1,.36,1),transform .95s cubic-bezier(.22,1,.36,1)}
.has-js .reveal.in-view{opacity:1;transform:translateY(0)}

/* Staggers within repeating groups so items cascade rather than pop together */
.cat-block.reveal:nth-child(2){transition-delay:.05s}
.cat-block.reveal:nth-child(3){transition-delay:.1s}
.cat-block.reveal:nth-child(4){transition-delay:.15s}
.cat-block.reveal:nth-child(5){transition-delay:.2s}
.cat-block.reveal:nth-child(6){transition-delay:.25s}
.faq-col details.reveal:nth-child(2){transition-delay:.07s}
.faq-col details.reveal:nth-child(3){transition-delay:.14s}
.faq-col details.reveal:nth-child(4){transition-delay:.21s}
.faq-col details.reveal:nth-child(5){transition-delay:.28s}
.faq-col details.reveal:nth-child(6){transition-delay:.35s}
.faq-col details.reveal:nth-child(n+7){transition-delay:.35s}

/* Hero entrance: runs once, after the preloader lifts */
.has-js .hero-in > div > *,.has-js .hero-in .rack{
  opacity:0;transform:translateY(22px);
  transition:opacity 1.1s cubic-bezier(.22,1,.36,1),transform 1.1s cubic-bezier(.22,1,.36,1);
}
.hero-ready .hero-in > div > *,.hero-ready .hero-in .rack{opacity:1;transform:none}
.has-js .hero-in > div > *:nth-child(2){transition-delay:.09s}
.has-js .hero-in > div > *:nth-child(3){transition-delay:.18s}
.has-js .hero-in > div > *:nth-child(4){transition-delay:.27s}
.has-js .hero-in > div > *:nth-child(5){transition-delay:.36s}
.has-js .hero-in .rack{transition-delay:.24s}
.three .reveal:nth-child(2){transition-delay:.11s}
.three .reveal:nth-child(3){transition-delay:.22s}
.steps .reveal:nth-child(2){transition-delay:.08s}
.steps .reveal:nth-child(3){transition-delay:.16s}
.price-grid .reveal:nth-child(2){transition-delay:.11s}
.price-grid .reveal:nth-child(3){transition-delay:.22s}
.r-grid .reveal:nth-child(2){transition-delay:.09s}
.r-grid .reveal:nth-child(3){transition-delay:.18s}
.r-grid .reveal:nth-child(4){transition-delay:.27s}

/* ---------- language toggle ---------- */
/* Sits next to the theme button and matches its footprint, so the two read
   as a pair of display settings rather than one control plus a link. */
.lang-btn{
  display:inline-flex;align-items:center;justify-content:center;
  height:36px;min-width:54px;padding:0 12px;
  border:1px solid var(--rule);border-radius:100px;background:transparent;
  color:var(--slate);font-family:var(--body);font-size:12.5px;font-weight:600;
  letter-spacing:.02em;cursor:pointer;
  /* Two Han glyphs are wider than two Latin caps and were wrapping inside the
     pill. nowrap plus a min-width that fits 中文 keeps it on one line and
     stops the nav reflowing when the label switches. */
  white-space:nowrap;line-height:1;
  transition:color .26s ease,border-color .26s ease,background .26s ease;
}
.lang-btn:hover{color:var(--ink);border-color:var(--rule-hover);background:var(--band)}
.lang-btn:focus-visible{outline:2.5px solid var(--marker);outline-offset:2px}
/* The Chinese label is two wide glyphs; the English one is two narrow caps.
   A fixed min-width keeps the nav from reflowing when it switches. */
[data-lang="zh"] .lang-btn{font-family:inherit}

/* ---------- on AI: two-column comparison ---------- */
/* The axis is what AI is good at versus what it can't cover — not us versus
   it. A vendor-style "us good / them bad" table would contradict the whole
   point of the section, and the left column has to carry real weight or the
   pro-AI half reads as a disclaimer. */
.ai-head{text-align:center;max-width:60ch;margin:0 auto clamp(32px,4vw,46px)}
.ai-badge{
  display:inline-flex;align-items:center;gap:7px;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;
  color:var(--marker);border:1px solid var(--rule);border-radius:100px;padding:6px 14px;
}
.ai-badge svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ai-head h2{margin:18px 0 0}
.ai-head .lede{margin:16px auto 0}

.ai-cols{display:grid;grid-template-columns:1fr 1fr;gap:clamp(14px,2vw,18px);align-items:start}
@media(max-width:820px){.ai-cols{grid-template-columns:1fr}}

.ai-col{border:1px solid var(--rule);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;height:100%}
.ai-col-head{padding:22px 24px 18px}
.ai-col-title{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.ai-col h3{font-family:var(--display);font-weight:800;font-size:17px;letter-spacing:-.01em;margin:0}
.ai-col-desc{margin:9px 0 0;font-size:14.5px;line-height:1.6;color:var(--label)}
[data-theme="dark"] .ai-col-desc{color:var(--slate)}
.ai-sep{height:1px;background:var(--rule);margin:0}
.ai-col ul{list-style:none;margin:0;padding:20px 24px 28px;display:flex;flex-direction:column;gap:13px;flex:1}
.ai-col li{display:flex;align-items:flex-start;gap:11px;font-size:14.5px;line-height:1.6}
.ai-ico{
  flex:none;width:17px;height:17px;margin-top:2px;border-radius:4px;
  display:grid;place-items:center;
}
.ai-ico svg{width:11px;height:11px;fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}

/* Left column carries the accent, the way the reference gives its
   recommended card a ring. Here that emphasis is on the pro-AI side. */
.ai-col.is-yes{border-color:var(--marker);box-shadow:0 0 0 3px rgba(231,111,81,.1)}
.ai-col.is-yes .ai-ico{background:var(--marker)}
.ai-col.is-yes .ai-ico svg{stroke:var(--always-light)}
.ai-col.is-yes li{color:var(--ink)}
.ai-pill{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;
  background:var(--highlight);color:var(--hl-text);padding:4px 9px;border-radius:100px;
}

.ai-col.is-no{background:var(--band)}
[data-theme="dark"] .ai-col.is-no{background:rgba(0,0,0,.22)}
.ai-col.is-no .ai-ico{background:var(--rule)}
.ai-col.is-no .ai-ico svg{stroke:var(--label)}
[data-theme="dark"] .ai-col.is-no .ai-ico svg{stroke:var(--slate)}
.ai-col.is-no li{color:var(--label)}
[data-theme="dark"] .ai-col.is-no li{color:var(--slate)}

/* The two CTA buttons that used to close out each column (ai-col-foot) were
   removed — this section reads as a comparison to sit with, not a funnel
   into two more links, and dropping them let both columns end on the list
   itself instead of a slab of button. */

/* Blur-out-up in the AI columns. The two lists are meant to be read against
   each other, so the rows resolve one at a time instead of the column landing
   as a slab — which is also what stops six near-identical lines from reading
   as a wall. The column only fades; the rows and the h3 carry the motion. */
.has-js .ai-col.reveal{transform:none}
.has-js .ai-col li{
  opacity:0;transform:translateY(10px);filter:blur(6px);
  transition:opacity var(--blur-dur) var(--blur-ease),
             transform var(--blur-dur) var(--blur-ease),
             filter var(--blur-dur) var(--blur-ease);
}
.has-js .ai-col.in-view li{opacity:1;transform:none;filter:blur(0)}
/* Starts after the column head's own words have run. */
.has-js .ai-col li:nth-child(1){transition-delay:.16s}
.has-js .ai-col li:nth-child(2){transition-delay:.23s}
.has-js .ai-col li:nth-child(3){transition-delay:.30s}
.has-js .ai-col li:nth-child(4){transition-delay:.37s}
.has-js .ai-col li:nth-child(5){transition-delay:.44s}
.has-js .ai-col li:nth-child(6){transition-delay:.51s}
.has-js .ai-col li:nth-child(n+7){transition-delay:.58s}

.ai-floor{margin:clamp(26px,3.4vw,36px) auto 0;max-width:72ch;text-align:center;font-size:15px;line-height:1.7;color:var(--label)}
[data-theme="dark"] .ai-floor{color:var(--slate)}
.ai-floor a{color:var(--marker);text-decoration:underline;text-underline-offset:3px}

/* ---------- card spotlight ----------
   A soft highlight that follows the cursor across a card. The alpha is a
   registered custom property so it can actually be transitioned — a plain
   --var can't be animated, and background-image isn't transitionable either,
   so fading the colour stop is the only way to get it to ease in.
   Deliberately faint and untinted beyond the existing accent: the cards
   already carry a hover lift and border change, and this is meant to sit
   under those rather than compete. */
@property --spot{syntax:'<number>';initial-value:0;inherits:false}
details.tutor,.price,.tcard,.ref-card,.ai-col,.cm-role{
  --spot:0;--mx:50%;--my:50%;
  background-image:radial-gradient(
    260px circle at var(--mx) var(--my),
    rgba(231,111,81,var(--spot)),
    transparent 68%
  );
  transition:border-color .26s ease,transform .26s cubic-bezier(.22,1,.36,1),
             box-shadow .3s ease,--spot .34s ease;
}
[data-theme="dark"] details.tutor,[data-theme="dark"] .price,[data-theme="dark"] .tcard,
[data-theme="dark"] .ref-card,[data-theme="dark"] .ai-col,[data-theme="dark"] .cm-role{
  background-image:radial-gradient(
    260px circle at var(--mx) var(--my),
    rgba(233,131,95,var(--spot)),
    transparent 68%
  );
}
@media(hover:hover){
  details.tutor:hover,.price:hover,.tcard:hover,.ref-card:hover,.ai-col:hover,.cm-role:hover{--spot:.07}
}
/* Touch devices never get a pointer position, so the gradient would sit
   frozen at the card's centre. Off entirely there. */
@media(hover:none),(pointer:coarse),(prefers-reduced-motion:reduce){
  details.tutor,.price,.tcard,.ref-card,.ai-col,.cm-role{background-image:none}
}


/* ==========================================================
   Theme switch — segmented pill with a thumb that slides
   between the two halves. Driven entirely by the [data-theme]
   attribute the existing toggle script already sets, so there
   is no new JS: the button keeps its id and its click handler.
   The visible icon is always the half the thumb is NOT sitting
   on, i.e. the mode you would switch to.
   ========================================================== */
.theme-sw{
  position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-items:center;
  width:60px;height:32px;flex:none;padding:0;
  background:var(--band);border:1px solid var(--rule);border-radius:100px;
  cursor:pointer;color:var(--ink);-webkit-tap-highlight-color:transparent;
  transition:border-color .34s ease,background .34s ease;
}
@media(hover:hover){.theme-sw:hover{border-color:var(--ink)}}
.theme-sw:focus-visible{outline:2px solid var(--marker);outline-offset:2px}
/* 50% of the padding box minus the 3px inset, so the thumb clears both
   ends by the same 3px at either extreme of its travel. */
.theme-sw-thumb{
  position:absolute;top:3px;left:3px;width:calc(50% - 3px);height:calc(100% - 6px);
  border-radius:100px;background:var(--ink);
  transition:transform .34s cubic-bezier(.16,1,.3,1);
}
[data-theme="dark"] .theme-sw-thumb{transform:translateX(100%)}
.theme-sw-ico{
  position:relative;z-index:1;display:grid;place-items:center;
  transition:opacity .26s ease,transform .34s cubic-bezier(.16,1,.3,1);
}
.theme-sw-ico svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.9;
  stroke-linecap:round;stroke-linejoin:round}
.theme-sw .is-sun{opacity:0;transform:scale(.55)}
.theme-sw .is-moon{opacity:.72;transform:none}
[data-theme="dark"] .theme-sw .is-sun{opacity:.72;transform:none}
[data-theme="dark"] .theme-sw .is-moon{opacity:0;transform:scale(.55)}
@media(prefers-reduced-motion:reduce){.theme-sw-thumb,.theme-sw-ico{transition:none}}
@media(max-width:400px){.nav-in .theme-sw{display:none}}

/* ==========================================================
   Nav v2 - a full-bleed, generously spaced bar that collapses
   into a floating pill once the page scrolls.

   The old bar inherited .wrap's 1180px max-width while its own
   contents measured ~1266px, so it overflowed its container at
   every width from 980px up and the CTA was clipped off the
   right edge. Letting the bar span the viewport is where the
   room to space the entries out actually comes from.

   .nav is now only a positioning wrapper: the painted surface
   lives on .nav-in so it can morph from a full-width bar into
   a pill. Keeping backdrop-filter off .nav matters for more
   than tidiness - an ancestor that has one becomes a backdrop
   root, and a nested backdrop-filter then samples an empty
   backdrop, so the pill could not frost at all while .nav was
   the thing doing the blurring.
   ========================================================== */
.nav{
  --nav-gut:clamp(16px,3.2vw,44px);
  --nav-pill:1240px;
  /* The pill gets a surface of its own rather than reusing --card: every
     card on the page already uses that, so the floating bar read as one
     more card drifting past. Translucent and saturated, so it picks up
     whatever is behind it instead of matching a flat surface. */
  --pill-bg:rgba(255,255,255,.86);
  --pill-ring:rgba(54,69,79,.16);
  --pill-shadow:0 1px 2px rgba(54,69,79,.07),
                0 10px 24px -10px rgba(54,69,79,.22),
                0 28px 56px -26px rgba(54,69,79,.5);
  /* --nav-dur and --nav-ease now live on :root (one duration and one curve
     for the entire morph, so the height, the width, the corners and the
     surface all arrive on the same frame; the curve leaves almost no
     velocity in the first few hundred milliseconds, which is what stops
     the bar reading as a snap the instant the threshold is crossed). They
     moved up there so the promo bar can ride the identical timing. */
  background:transparent;border-bottom:0;
  backdrop-filter:none;-webkit-backdrop-filter:none;
  /* Sits below the promo bar while it's up, and rises to the top edge as
     the bar slides away — same duration as the morph, so the nav arriving
     at top:0 and the pill forming are one movement rather than two. */
  top:var(--promo-h);
  transition:padding var(--nav-dur) var(--nav-ease),
             top var(--nav-dur) var(--nav-ease);
}
[data-theme="dark"] .nav{
  --pill-bg:rgba(43,52,59,.88);
  --pill-ring:rgba(255,255,255,.14);
  --pill-shadow:0 1px 2px rgba(0,0,0,.45),
                0 12px 28px -12px rgba(0,0,0,.6),
                0 30px 60px -28px rgba(0,0,0,.9);
}
.nav-in{
  /* 100vw rather than none. `none` is not a length, so max-width:none to
     1240px is a discrete jump: the bar snapped to pill width on the very
     first frame while everything else eased. That single non-animatable
     value was most of why the morph felt abrupt no matter how long the
     duration got. */
  max-width:100vw;padding:0 var(--nav-gut);gap:clamp(10px,1vw,14px);
  background:var(--nav-bg);
  /* saturate(1) is a no-op that matters: filter lists interpolate only
     when both sides hold the same functions in the same order, so
     without it the blur stepped straight to 26px at the halfway mark
     instead of ramping. */
  backdrop-filter:blur(10px) saturate(1);
  -webkit-backdrop-filter:blur(10px) saturate(1);
  border:1px solid transparent;border-bottom-color:var(--rule);border-radius:0;
  /* border-radius was missing from this list, so the corners went fully
     round on frame one while the width was still travelling. */
  transition:height var(--nav-dur) var(--nav-ease),
             max-width var(--nav-dur) var(--nav-ease),
             padding var(--nav-dur) var(--nav-ease),
             gap var(--nav-dur) var(--nav-ease),
             border-radius var(--nav-dur) var(--nav-ease),
             background var(--nav-dur) var(--nav-ease),
             border-color var(--nav-dur) var(--nav-ease),
             box-shadow var(--nav-dur) var(--nav-ease),
             backdrop-filter var(--nav-dur) var(--nav-ease);
}
/* Auto margins on both sides park the link group in the middle of the
   leftover space rather than jammed up against the CTA, and the gap
   scales with the viewport so wide screens genuinely read as spaced out.
   That leftover space isn't symmetric, though: the logo on the left is
   far narrower than the auth link + language toggle + theme switch + CTA
   cluster on the right, so auto-margin centering (which splits the same
   leftover space evenly) lands the link group visibly left of true
   center. The gap is remarkably constant across desktop widths (~144px,
   measured 143-145px from 1280 to 1920) because both flanking clusters
   are fixed-content and don't grow with viewport.

   144px is what true centering costs, but that room only exists on wide
   screens: the slack between the last link and "Log in / Sign up" is
   308px at 1920 and 20px at 1280, so a flat shift centers beautifully at
   1920 and drives the links straight through the auth link at 1440.
   Hence the ramp. It starts at 1280 (the desktop-nav breakpoint, where
   there is no room at all), climbs at 0.32px per px of viewport — the
   steepest slope that still leaves ~26px of breathing room at 1366, the
   tightest width that has any — and caps at 144px, which is reached
   around 1730px and is dead centre from there up.

   Scrolled/pill mode has its own, tighter geometry and cancels the shift
   (see .nav.scrolled .nav-links below); the mobile dropdown cancels it
   too, since nav-links stops being an inline flex sibling there. */
.nav-links{
  margin-left:auto;margin-right:auto;gap:clamp(13px,1.5vw,27px);
  transform:translateX(clamp(0px, (100vw - 1280px) * 0.32, 144px));
  transition:gap var(--nav-dur) var(--nav-ease),
             transform var(--nav-dur) var(--nav-ease);
}

/* Scrolled: the wrapper only insets, and the inner row becomes a frosted
   floating pill. box-shadow:none clears the original .nav.scrolled shadow,
   which would otherwise draw a full-width band behind a transparent bar. */
.nav.scrolled{padding:13px 12px 0;box-shadow:none}
.nav.scrolled .nav-in{
  max-width:var(--nav-pill);padding-left:20px;padding-right:10px;gap:10px;
  background:var(--pill-bg);
  backdrop-filter:blur(26px) saturate(1.6);
  -webkit-backdrop-filter:blur(26px) saturate(1.6);
  border-color:var(--pill-ring);border-radius:100px;
  box-shadow:var(--pill-shadow);
}
.nav.scrolled{top:0}
.nav.scrolled .nav-links{gap:16px;transform:translateX(0)}
/* The progress bar rides the top edge once the bar is floating, rather
   than underlining a pill it no longer spans. */
.nav.scrolled .scroll-progress{top:0;bottom:auto}

/* ==========================================================================
   PROMO BAR
   --------------------------------------------------------------------------
   A thin announcement strip above the nav while the page is at the top,
   which slides away as the nav collapses into its pill. Deliberately not
   inside .nav: that element morphs into a floating rounded pill, and a
   full-width strip living inside it would have to un-morph on the same
   frame, fighting the transition the whole way.

   It ships in the markup on every deploy and is display:none until the
   head script confirms the window is open, so an expired promo can never
   flash before the date check catches it.
   ========================================================================== */
html.promo-on{--promo-h:42px}
.promo{display:none}
html.promo-on .promo{
  display:block;position:fixed;top:0;left:0;right:0;
  /* Under the nav's 60: as the nav travels up to top:0 it should pass over
     the retreating strip, not under it. */
  z-index:55;
  height:var(--promo-h);background:var(--promo-bg);color:var(--promo-fg);
  transition:transform var(--nav-dur) var(--nav-ease);
}
/* Sibling combinator, which is why .promo sits after </nav> in the markup:
   the scroll state already lives on .nav and doesn't need a second class
   pushed onto another element to mirror it. */
.nav.scrolled ~ .promo{transform:translateY(-100%)}

.promo-in{
  height:100%;display:flex;align-items:center;justify-content:center;
  gap:12px;padding:0 44px;text-decoration:none;color:inherit;
}
.promo-msg{font-size:13.5px;font-weight:500;white-space:nowrap}
.promo-em{color:var(--promo-accent);font-weight:600}
.promo-go{
  font-size:13.5px;font-weight:600;white-space:nowrap;
  border-bottom:1px solid rgba(248,249,250,.38);padding-bottom:1px;
  transition:border-color .3s ease;
}
.promo-in:hover .promo-go{border-bottom-color:currentColor}
.promo-x{
  position:absolute;right:8px;top:50%;transform:translateY(-50%);
  background:none;border:0;color:var(--promo-fg);opacity:.5;
  font-size:19px;line-height:1;cursor:pointer;padding:6px 9px;
  transition:opacity .3s ease;
}
.promo-x:hover{opacity:1}
/* The short copy carries the offer and drops the deadline — the full line
   can't hold one line below ~700px, and a bar that wraps to two would make
   --promo-h a lie and misalign everything hanging off it. */
.promo-short{display:none}
@media(max-width:700px){
  .promo-full{display:none}
  .promo-short{display:inline}
  .promo-go{display:none}
  .promo-in{padding:0 38px 0 16px}
  .promo-msg{font-size:12.5px}
}

/* Log in leaves the link group and joins the right-hand cluster. */
.nav-auth-desk{
  font-size:14.5px;font-weight:500;text-decoration:none;color:var(--ink);
  opacity:.68;white-space:nowrap;flex:none;transition:opacity .3s ease;
}
.nav-auth-desk:hover{opacity:1}

/* With the full "Log in / Sign up" label the desktop row needs ~1265px to
   lay out without clipping, so the hamburger takes over at 1280 rather
   than the original 980 — below which the old bar was overflowing, not
   fitting. Measured, not guessed: 1280 leaves 14px, 1260 clips. */
@media(min-width:1280px){.nav-auth-mob{display:none}}
@media(max-width:1279px){
  .nav-auth-desk{display:none}
  .nav-toggle{display:flex}
  .nav-hl{display:none}
  .nav-links{
    display:none;position:absolute;top:var(--navh);left:0;right:0;
    flex-direction:column;align-items:stretch;gap:0;margin:0;transform:none;
    /* Solid: this panel hangs below the bar's own surface, over live page
       content, so it has to be opaque in its own right. */
    background:var(--paper);
    border-bottom:1px solid var(--rule);
    padding:8px var(--nav-gut) 16px;
    box-shadow:0 18px 30px -20px rgba(var(--shadow),.25);
  }
  .nav-links.open{display:flex}
  .nav-links a{padding:13px 0;border-bottom:1px solid var(--rule);font-size:15.5px}
  .nav-links a:last-child{border-bottom:0}
  /* No pill on small screens: the plain full-width bar reads better and
     gives the dropdown a straight edge to attach to. */
  .nav.scrolled{padding:0}
  .nav.scrolled .nav-in{
    max-width:none;padding:0 var(--nav-gut);border-radius:0;
    background:var(--nav-bg);
    backdrop-filter:blur(10px) saturate(1);
    -webkit-backdrop-filter:blur(10px) saturate(1);
    border-color:transparent;border-bottom-color:var(--rule);
    box-shadow:none;
  }
  .nav.scrolled .scroll-progress{top:auto;bottom:-1px}
}

/* This block sits last in the sheet, so it overrides the reduced-motion
   rule further up. Re-assert the opt-out for the elements it touches. */
@media(prefers-reduced-motion:reduce){
  .nav,.nav-in,.nav-links{transition:none}
}

/* ==========================================================
   Multi-page layout (Sept 2026). The site used to be one long
   page; these rules cover what the split added.
   ========================================================== */
/* The first section of a sub-page carries the page's h1. */
.h1-page{font-size:clamp(34px,5.2vw,60px)}
.story .h1-page{color:#fff;font-size:clamp(30px,4.4vw,50px);max-width:18ch}
/* Current page in the nav. */
.nav-links a[aria-current="page"]{opacity:1;font-weight:600}
a.lang-btn{text-decoration:none}

/* Home: featured tutors + link through to the full roster. */
.more-link-wrap{display:flex;justify-content:center;margin-top:26px}

/* Home: price snapshot. */
.price-snap{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
.snap-card{background:var(--card);border:1px solid var(--rule);border-radius:8px;padding:24px 24px 22px}
.snap-label{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);margin:0 0 14px}
.snap-price{font-family:var(--display);font-weight:800;font-size:40px;letter-spacing:-.03em;line-height:1;margin:0}
.snap-from{font-family:var(--body);font-size:14px;font-weight:500;letter-spacing:0;color:var(--slate);margin-right:4px}
.snap-unit{font-family:var(--mono);font-size:12px;font-weight:400;color:var(--slate);margin-left:4px;letter-spacing:.04em}
.snap-sub{font-size:14px;color:var(--slate);margin:12px 0 0}
.snap-foot{display:flex;flex-wrap:wrap;gap:10px 28px;margin:24px 0 0}
.text-link{color:var(--marker);font-weight:600;text-decoration:none;border-bottom:1px solid transparent;transition:border-color .26s ease}
.text-link:hover{border-bottom-color:currentColor}
@media(max-width:820px){.price-snap{grid-template-columns:1fr}}
