/* NCERT TRAPS — motion layer
   Loaded on every page, after style.css.

   Three rules this file keeps to:

   1. NOTHING IS HIDDEN BY DEFAULT. Every animation either starts from a
      visible state or is wrapped in @supports, so a browser that cannot run
      it shows the page exactly as before rather than a blank column. The
      usual "opacity:0 until JavaScript says otherwise" trick fails open in
      the worst possible way - if the script is slow, blocked or broken, the
      page has no content in it.

   2. THE HERO'S LARGEST ELEMENTS NEVER FADE. Largest Contentful Paint is
      measured when the biggest thing in view renders, and an element at
      opacity 0 has not rendered. The cover and the headline therefore move
      but never fade; only the smaller text around them fades in.

   3. MOTION IS OPTIONAL. Everything here is switched off for anyone who has
      asked their device for reduced motion - a real setting for students who
      get motion sickness or migraines, and this is a study site they will
      sit in front of for an hour at a time. */

:root{
  /* One easing curve for the whole site, so nothing feels borrowed from
     somewhere else. A gentle overshoot-free ease-out: things arrive and
     settle rather than bouncing. */
  --ease:cubic-bezier(.22,.61,.36,1);
  --ease-soft:cubic-bezier(.4,0,.2,1);
}

/* ------------------------------------------------------------------ *
   1. The hero
 * ------------------------------------------------------------------ */

/* A slow wash of the two brand colours behind the headline. It sits at very
   low opacity and well below the text, so contrast is untouched - measured
   against --muted on the lightest point of the wash, still WCAG AA.

   Deliberately not a looping animation on mobile: it would keep a compositor
   layer awake and eat battery on a phone that is being used for revision. */
.hero{position:relative;isolation:isolate;overflow:hidden}
.hero::before{
  content:"";position:absolute;z-index:-1;inset:-40% -10% auto -10%;height:120%;
  background:
    radial-gradient(48% 54% at 18% 28%, rgba(18,118,109,.34), transparent 68%),
    radial-gradient(44% 50% at 82% 18%, rgba(192,39,26,.22), transparent 70%),
    radial-gradient(52% 46% at 64% 82%, rgba(51,78,160,.20), transparent 68%);
  filter:blur(10px);
  animation:drift 26s var(--ease-soft) infinite alternate;
}
@keyframes drift{
  0%  {transform:translate3d(0,0,0) scale(1)}
  50% {transform:translate3d(-2.5%,1.5%,0) scale(1.06)}
  100%{transform:translate3d(2%,-1%,0) scale(1.03)}
}
@media(max-width:700px){.hero::before{animation:none}}

/* The wash is mixed against a near-white ground. Over a near-black one the
   same alphas barely register, so the dark theme gets its own, with a wider
   blur, or the hero is just a flat black rectangle.

   The teal is held well below the other two here. It is the largest of the
   three and sits over the headline; at equal strength it tinted the whole
   upper page green, which was the one thing the neutral greys were changed
   to stop. */
:root[data-theme="dark"] .hero::before{
  background:
    radial-gradient(48% 54% at 18% 28%, rgba(107,166,156,.13), transparent 68%),
    radial-gradient(44% 50% at 82% 18%, rgba(255,122,102,.13), transparent 70%),
    radial-gradient(52% 46% at 64% 82%, rgba(138,160,240,.13), transparent 68%);
  filter:blur(26px);
}

/* The entrance. Staggered so the eye is led down the column in the order the
   copy wants to be read: label, headline, promise, buttons, reassurance. */
.hero .eyebrow,
.hero .lede,
.hero .buy-actions,
.hero .trust{animation:rise .62s var(--ease) both}
.hero .eyebrow     {animation-delay:.02s}
.hero .lede        {animation-delay:.14s}
.hero .buy-actions {animation-delay:.22s}
.hero .trust       {animation-delay:.30s}

@keyframes rise{
  from{opacity:0;transform:translate3d(0,14px,0)}
  to  {opacity:1;transform:none}
}

/* Rule 2: the headline and the cover are the LCP candidates, so they move
   without ever being invisible. */
.hero h1{animation:lift .72s var(--ease) both}
.hero-art img{animation:cover-in .9s var(--ease) both}
@keyframes lift{
  from{transform:translate3d(0,18px,0)}
  to  {transform:none}
}

/* A thick red stroke drawn under the second line, left to right, once the
   headline has landed. The mark sits BEHIND the words rather than recolouring
   them, so the teal stays teal and the contrast the palette was tuned for is
   untouched - a red highlight through teal text would have failed AA. */
.hero h1 em{
  background-image:linear-gradient(var(--red),var(--red));
  background-repeat:no-repeat;
  background-position:0 88%;
  background-size:0 .13em;
  animation:stroke .68s var(--ease) .52s both;
  padding-bottom:.04em;
}
@keyframes stroke{
  from{background-size:0 .13em}
  to  {background-size:100% .13em}
}
@keyframes cover-in{
  from{transform:translate3d(0,16px,0) scale(.985) rotate(-.6deg)}
  to  {transform:none}
}

/* A book has weight. Once it has landed it breathes, very slightly, which is
   what stops a still product shot looking like a placeholder. */
.hero-art img{will-change:transform}
.hero-art:hover img{transform:translate3d(0,-6px,0) scale(1.012);
  box-shadow:0 26px 60px var(--shadow)}
.hero-art img{transition:transform .5s var(--ease),box-shadow .5s var(--ease)}

/* ------------------------------------------------------------------ *
   2. Reveal on scroll, with no JavaScript
 * ------------------------------------------------------------------ */

/* Scroll-driven animations run on the compositor and need no observer, no
   script and no "hidden until proven visible" state. Browsers without them
   skip the whole block and simply show the content, which is the correct
   fallback and costs nothing. */
@supports (animation-timeline: view()){
  .card,
  .plan,
  .stat,
  .qtypes li,
  .rev,
  .shots img,
  .faq > *,
  .band .wrap > *{
    animation:reveal .7s var(--ease) both;
    animation-timeline:view();
    /* Start as the element enters the lower fifth of the viewport and finish
       well before it reaches the middle, so nothing is still animating by the
       time it is being read. */
    animation-range:entry 0% entry 62%;
  }
  @keyframes reveal{
    from{opacity:0;transform:translate3d(0,26px,0)}
    to  {opacity:1;transform:none}
  }

  /* A grid of cards arriving in perfect unison looks mechanical. A few
     nth-child offsets are enough to break the line without anyone noticing
     why it feels better. */
  .grid > *:nth-child(2n),.plans > *:nth-child(2n){animation-range:entry 4% entry 66%}
  .grid > *:nth-child(3n),.plans > *:nth-child(3n){animation-range:entry 8% entry 70%}

  /* The header earns a shadow only once the page has actually moved, so the
     top of the page stays flat and clean. */
  .site-head{
    animation:head-settle linear both;
    animation-timeline:scroll();
    animation-range:0 90px;
  }
  @keyframes head-settle{
    from{box-shadow:0 0 0 transparent}
    to  {box-shadow:0 6px 22px var(--shadow-soft)}
  }
}

/* ------------------------------------------------------------------ *
   3. Things you touch
 * ------------------------------------------------------------------ */

.card,.plan{transition:transform .32s var(--ease),box-shadow .32s var(--ease),
  border-color .32s var(--ease)}
.card:hover,.plan:hover{transform:translate3d(-3px,-6px,0);
  box-shadow:10px 12px 0 var(--lift);border-color:var(--brand)}
.plan--best:hover{border-color:var(--brand-lift)}

/* A light sweeping across the button on hover. Pure CSS, one pseudo-element,
   and it is clipped to the button so it cannot paint over anything else. */
.btn{position:relative;overflow:hidden;isolation:isolate}
.btn::after{
  content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(105deg,transparent 38%,rgba(255,255,255,.22) 50%,transparent 62%);
  transform:translateX(-120%);
}
.btn:hover::after{transform:translateX(120%);transition:transform .7s var(--ease-soft)}
.btn--ghost::after{background:linear-gradient(105deg,transparent 38%,rgba(14,92,85,.10) 50%,transparent 62%)}

/* The nav underline grows from the left rather than appearing all at once. */
.nav a{position:relative}
.nav a::after{content:"";position:absolute;left:0;right:0;bottom:-6px;height:2px;
  background:var(--red);transform:scaleX(0);transform-origin:left;
  transition:transform .28s var(--ease)}
.nav a:hover::after,.nav a[aria-current="page"]::after{transform:scaleX(1)}
/* The header's call to action is a button, not a link, and does not want one. */
.nav a.btn::after{display:none}

/* Question-format rows and review cards, on hover, tilt the tiniest amount
   toward the cursor's side of the page. Small enough to feel like paper. */
.qtypes li{transition:background .3s var(--ease)}
.qtypes li:hover{background:var(--ink-3)}

/* ------------------------------------------------------------------ *
   4. The counting numbers
 * ------------------------------------------------------------------ */

/* motion.js swaps in rising digits when the strip scrolls into view. The
   final number is in the HTML, so with no JavaScript - or before it runs -
   the real figure is what is on screen. Tabular figures stop the width
   jittering as the digits change. */
.stat b{font-variant-numeric:tabular-nums;
  font-size:clamp(2.1rem,3.4vw,2.9rem);letter-spacing:-.035em;line-height:1.05}
.stat{transition:background .3s var(--ease);position:relative}
.stat:hover{background:var(--ink-3)}
/* A red rule wipes across the top of each figure as it arrives, so the strip
   reads as four headlines rather than four table cells. */
.stat::before{content:"";position:absolute;top:0;left:0;height:3px;width:100%;
  background:var(--red);transform:scaleX(0);transform-origin:left}
@supports (animation-timeline: view()){
  .stat::before{animation:wipe .6s var(--ease) both;
    animation-timeline:view();animation-range:entry 10% entry 55%}
  @keyframes wipe{to{transform:scaleX(1)}}
}

/* ------------------------------------------------------------------ *
   5. Off, for anyone who asked for it off
 * ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
  .hero::before{animation:none}
  .hero-art:hover img{transform:none}
  .card:hover,.plan:hover{transform:none}
}

/* ------------------------------------------------------------------ *
   6. The chapter ticker
 * ------------------------------------------------------------------ */

/* Every chapter, sliding slowly past under the hero. It answers "is my
   chapter in this book?" before the visitor has scrolled or opened anything.

   The markup prints the list twice. The row slides exactly -50%, which is the
   width of one copy, so at the moment it snaps back the second copy is
   already sitting where the first one started and the seam cannot be seen.
   Any distance other than -50% shows a jump. */
/* The ticker is the last thing in the hero, so it closes the section itself:
   without this the hero's own bottom border draws a second line a padding's
   distance below the strip, and the two read as a mistake. Browsers without
   :has() simply keep the old spacing, which is tidy rather than wrong. */
.hero:has(.ticker){padding-bottom:0;border-bottom:0}

.ticker{
  margin-top:clamp(26px,4vw,44px);
  /* Ink black, the colour of the cover. The page is a pale clinical ground
     from top to bottom, and one hard dark band under the hero is what stops
     it reading as a document. White on this ground is 15:1 - the strongest
     contrast anywhere on the site.

     --band, not --bone. --bone is the TEXT colour, which the dark theme flips
     to near-white: this band would have become a white strip with white
     writing on it. --band is ink-black in both themes. */
  background:var(--band);
  overflow:hidden;
}
/* No edge fade, on purpose, and it took two wrong attempts to get here.
   Masking the band dissolved its black ends into the page, so the strip
   looked torn off; moving the mask onto the row made it travel WITH the row,
   which tiled it and put fade bands in the middle of words. A solid band
   clipped hard at the screen edge is both correct and what a ticker has
   always looked like. */
.ticker-row{display:flex;width:max-content;animation:ticker 38s linear infinite}
.ticker-set{display:flex;list-style:none;margin:0;padding:0}
.ticker-set li{
  font-family:var(--ui);font-size:.88rem;font-weight:800;
  letter-spacing:.14em;text-transform:uppercase;
  color:#fff;white-space:nowrap;padding:19px 0;margin:0;
  display:flex;align-items:center;
}
/* The separator is a dot in the brand colour rather than a slash, because a
   slash reads as "or" and these are nineteen things you get, not a choice. */
.ticker-set li::after{
  content:"";width:7px;height:7px;border-radius:50%;
  background:var(--red);margin:0 26px;flex:0 0 auto;
}
@keyframes ticker{
  from{transform:translate3d(0,0,0)}
  to  {transform:translate3d(-50%,0,0)}
}
/* Stop it while someone is trying to read a particular chapter name. */
.ticker:hover .ticker-row,
.ticker:focus-within .ticker-row{animation-play-state:paused}

@media (prefers-reduced-motion: reduce){
  /* Still readable, just no longer moving: the strip becomes an ordinary
     scrollable row that the visitor pushes themselves. */
  .ticker{overflow-x:auto}
  .ticker-row{animation:none}
}

/* ------------------------------------------------------------------ *
   7. Page furniture
 * ------------------------------------------------------------------ */

@supports (animation-timeline: scroll()){
  /* How far down the page you are, in red, across the very top. Needs no
     markup and no script: the bar is a pseudo-element on <body> and its
     width is driven straight off the scroll position by the compositor.
     Above the sticky header (50) but below the skip link (99), so keyboard
     users still get their focus outline over the top of everything. */
  body::before{
    content:"";position:fixed;top:0;left:0;z-index:70;
    width:100%;height:4px;background:var(--red);
    transform:scaleX(0);transform-origin:0 50%;
    animation:progress linear both;
    animation-timeline:scroll();
  }
  @keyframes progress{to{transform:scaleX(1)}}

  /* The rule trailing every section label draws itself in as the section
     arrives, instead of being there all along. */
  .eyebrow::after{transform:scaleX(0);transform-origin:left;
    animation:draw .8s var(--ease) both;
    animation-timeline:view();animation-range:entry 0% entry 50%}
  @keyframes draw{to{transform:scaleX(1)}}
}

/* A fine grain over the whole page. It is the difference between a screen
   that looks printed and one that looks like a template - at 3.5% it is felt
   rather than seen, and it is a single inline SVG, so it costs no request.
   Fixed, so it never repaints while scrolling, and click-through everywhere. */
body::after{
  content:"";position:fixed;inset:0;z-index:1;pointer-events:none;opacity:.035;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Grain reads far louder over a dark ground than a light one, so it is
   dialled back rather than left to speckle. */
:root[data-theme="dark"] body::after{opacity:.018}

@media (prefers-reduced-motion: reduce){
  /* The grain is texture, not motion, so it stays. The bar and the rules are
     motion, so they finish immediately rather than tracking the scroll. */
  body::before{animation:none;transform:scaleX(0)}
  .eyebrow::after{animation:none;transform:scaleX(1)}
  .hero h1 em{animation:none;background-size:100% .13em}
  .stat::before{animation:none;transform:scaleX(1)}
}
