/* ==========================================================================
   EdgeBook — shared marketing motion layer
   --------------------------------------------------------------------------
   One file, linked LAST in <head> on every site/ page, so these rules win over
   each page's own inline <style> without a single one of them being edited.
   Companion: /assets/js/motion.js

   Rules of the road (matching the React motion system):
     · Only transform-ish properties are animated — `translate` / `scale`, plus
       opacity and visibility. Nothing here changes layout, so nothing can
       cause a layout shift.
     · The standalone `translate` / `scale` properties are used instead of
       `transform`, because every page already animates `transform` on hover
       (.btn-accent:hover has translateY(-1px)). The individual properties
       COMPOSE with `transform`; overwriting `transform` would destroy it.
     · Duration ladder is the same five levels as the React tokens, set to the
       marketing surface (slightly more expressive than the dashboard).
   ========================================================================== */

:root{
  --m-dur-1:140ms;                              /* micro       */
  --m-dur-2:220ms;                              /* ui          */
  --m-dur-3:360ms;                              /* component   */
  --m-dur-4:460ms;                              /* interactive */
  --m-dur-5:720ms;                              /* showcase    */
  --m-ease-out:cubic-bezier(.22,1,.36,1);
  --m-ease-spring:cubic-bezier(.34,1.56,.64,1); /* the login switch's curve */
  --m-lift:3px;
  --m-reveal-y:14px;
  --m-spot:240px;

  /* Press squish — the dashboard's OWN numbers, and its own SHAPE.
     Its `Button` drives `SCALE.press` (0.975 — a 2.5 % squeeze, barely
     perceptible) with `SPRING.snappy`, and it uses that one spring in BOTH
     directions: `setTarget(0.975)` on pointerdown, `setTarget(1)` on pointerup.
     Nothing about it is asymmetric, and nothing about it passes its resting size.

     This block used to squish to 0.88 — a 12 % shrink, five times deeper — and
     release over 500 ms on `--m-ease-spring`, whose second control point sits at
     1.56. That pair is what produced the bounce the homepage showed: the button
     shrank visibly, then sprang back LARGER than its resting size before
     settling.

     A stylesheet cannot run a physics spring, so the stand-in is the app's own
     `EASE.snappy` — `cubic-bezier(0.2, 0.9, 0.2, 1)` — over the spring's ~220 ms
     settle (`--m-dur-2`), applied identically down and up. `SPRING.snappy` is
     stiffness/damping/mass (480/40/1), not a curve; it has no literal CSS twin,
     and `EASE.snappy` is the token that exists to stand in for it. */
  --m-press:.975;
  --m-press-dur:var(--m-dur-2);
  --m-press-ease:cubic-bezier(.2,.9,.2,1);

  /* Ripple. Longer than a press so it is still travelling when the button has
     already sprung back — the two together are what the dashboard's `Button`
     variant `primary` feels like. `--m-ripple-ink` is the resting opacity of
     the ink; it fades to 0 over the same duration. */
  --m-ripple-dur:550ms;
  --m-ripple-ink:0.28;

  /* Accent hover shades, DERIVED from each page's own `--accent` — the same
     contract as the dashboard's `--c-accent-hi`. Dark theme LIGHTENS on hover,
     light theme DARKENS: that is the convention that reads as "active" against
     both backgrounds. A literal hover colour instead snaps to one fixed shade
     the moment the pointer arrives — the homepage shipped `#e2ff6b` with no
     light-theme counterpart, so a light-mode CTA flashed lime over a dark green
     accent. */
  --accent-hi:color-mix(in srgb, var(--accent) 80%, white);
  /* The dashboard's primary hover glow: the accent itself at 40 %, not the
     16 % `--accent-glow` the pages use for pill and chip fills. */
  --accent-glow-hi:color-mix(in srgb, var(--accent) 40%, transparent);
}
:root[data-theme="light"]{
  --accent-hi:color-mix(in srgb, var(--accent) 82%, black);
}

/* --------------------------------------------------------------------------
   Buttons — press feedback, on every page, for free.
   One duration and one curve, used for the press AND the release, because that
   is what the dashboard's `Button` does with its press spring.

   This layer deliberately does NOT restyle the button SURFACE. Each page owns
   its own fill, border, text colour and hover lift; overriding them here
   (a frosted "liquid-glass" pane with a sweeping specular streak used to live
   here) fights the page's own design and has been removed.
   -------------------------------------------------------------------------- */
.btn, .btn-accent, .btn-ghost, .btn-sm{
  /* Explicit resting value — see the note on `scale` above. */
  scale:1;
  /* `scale` is the ONLY property that gets the press treatment: its duration
     and easing live in custom properties so `:active` can retarget just this
     one entry's TARGET without touching the rest of the list. Writing
     `transition:scale …` (or `transition-duration`) on `:active` instead would
     replace the whole list and silently kill the shadow/colour transitions —
     the exact bug this file already hit once. */
  /* 150 ms across the board — the dashboard's `duration-150`. The marketing
     surface used to fade background and shadow over 300 ms, which reads as a
     slow wash next to the dashboard's snap. Only `scale` keeps its own press
     curve. */
  transition:
    transform 150ms ease,
    scale var(--m-press-dur) var(--m-press-ease),
    box-shadow 150ms ease,
    background-color 150ms ease,
    border-color 150ms ease;
}

/* Driven from `:active`, not from JS pointer tracking — so keyboard
   Enter/Space gets the same squish, and there is no listener to leak.
   Only the TARGET is set here; the duration and curve stay on the base rule, so
   the return trip runs on exactly the same timing as the press. Retargeting the
   duration here is how this file previously grew an 80 ms-down / 220 ms-up
   asymmetry that the dashboard has never had — `Button` uses one spring both
   ways. */
.btn:active{
  scale:var(--m-press);
}

/* --------------------------------------------------------------------------
   Ripple — the one piece of the dashboard's primary button the pages lacked.
   `Button` variant `primary` is in the hook's FILLED set, so it lays an ink
   ripple under the pointer; the marketing buttons had the squish but not this.

   The host needs `position:relative; overflow:hidden` so the ink is clipped to
   the button's rounded box. JS adds both as a class, so a page that never runs
   motion.js is left exactly as it was. An `outline` focus ring is NOT clipped
   by `overflow`, so the keyboard affordance is unaffected.

   The ink is `currentColor`, which on `.btn-accent` is the dark label on a light
   accent fill — ink on paper. It is deliberately not applied to `.btn-ghost`:
   on a transparent button there is nothing for the ink to contrast against, and
   it reads as a rendering artefact rather than as contact.
   -------------------------------------------------------------------------- */
.m-ripple-host{ position:relative; overflow:hidden; }
.m-ripple{
  position:absolute; border-radius:50%; pointer-events:none;
  background:currentColor; opacity:var(--m-ripple-ink);
  /* `scale`, not `transform`, for the same reason the press uses it: the host
     animates `transform` on hover and the two must compose, not replace. */
  scale:0;
  animation:m-ripple var(--m-ripple-dur) var(--m-ease-out) forwards;
}
@keyframes m-ripple{
  to{ scale:1; opacity:0; }
}

/* --------------------------------------------------------------------------
   Primary CTA hover — the dashboard's contract, applied to `.btn-accent`.
   `Button` variant `primary` does exactly three things on hover and so does
   this: a derived shade of the accent, a 1px lift, and a soft accent glow.
   -------------------------------------------------------------------------- */
.btn-accent:hover{
  background:var(--accent-hi);
  box-shadow:0 8px 24px -6px var(--accent-glow-hi);
  transform:translateY(-1px);
}
/* The pages that DO ship a light-theme hover pin it to a literal
   (`:root[data-theme="light"] .btn-accent:hover{ background:#97c70a }`), which
   outranks a bare `.btn-accent:hover`. Match that specificity so the derived
   shade wins in light mode too. */
:root[data-theme="light"] .btn-accent:hover{
  background:var(--accent-hi);
}

/* --------------------------------------------------------------------------
   Secondary (ghost) hover — the dashboard's `ghost` variant fills with the
   hover shade and leaves the border exactly as it was. The pages instead
   brightened `border-color` on hover, which reads as the button growing an
   outline rather than responding to the pointer.

   `--bg-card-hover` is a dedicated hover shade but only some pages define it,
   hence the fallback — an undefined custom property here would otherwise make
   the hover background transparent on the pages that lack it.
   -------------------------------------------------------------------------- */
.btn-ghost:hover{
  background:var(--bg-card-hover, var(--bg-card));
  border-color:var(--border-strong);
}

/* --------------------------------------------------------------------------
   Magnetic — JS writes the offset into `translate`.
   The transition is what carries the element home; while the pointer is inside
   the element it is switched off (`.m-active`) so the follow is 1:1 instead of
   lagging a transition behind the cursor.
   -------------------------------------------------------------------------- */
/* The transition is what carries the element home; while the pointer is inside
   the element it is shortened to 0 s (`.m-active`) so the follow is 1:1 instead
   of lagging a transition behind the cursor.

   The duration lives in a CUSTOM PROPERTY for exactly that reason. Writing
   `transition:none` on `.m-active` (the obvious move) kills every other
   transition too — the press `scale` and the hover background start snapping,
   which silently removes the press feedback from the most important buttons on
   the page. Targeting the variable takes the translate duration to zero and
   leaves the rest of the list untouched. */
/* Cursor-follow now covers the hero visual only. Magnetism used to be applied to
   the CTAs as well; it was removed on request, and so was the liquid-glass
   surface that briefly replaced it. */
.m-follow{
  transition:
    transform .18s ease,
    translate var(--m-fol-dur, var(--m-dur-4)) var(--m-ease-spring),
    scale 110ms var(--m-ease-out),
    box-shadow .18s ease,
    background .18s ease,
    border-color .18s ease;
}
.m-follow.m-active{ --m-fol-dur:0s; }

/* --------------------------------------------------------------------------
   Spotlight — the same CSS-only approach as the React `MotionCard`: JS writes
   --mx/--my, the gradient lives in a pseudo-element so it can never sit on top
   of the card's own content or interfere with its background.
   `var(--accent-glow)` is theme-aware (lime 16% dark, olive 16% light), so the
   glow reads correctly in both themes with no second rule.
   -------------------------------------------------------------------------- */
.m-spot{ position:relative; }
.m-spot::after{
  content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:radial-gradient(var(--m-spot) circle at var(--mx,50%) var(--my,50%), var(--accent-glow), transparent 68%);
  opacity:0; transition:opacity var(--m-dur-2) var(--m-ease-out);
}
.m-spot:hover::after{ opacity:1; }

/* --------------------------------------------------------------------------
   Card lift — hover only. Deliberately a small shadow: animating a large one
   is the most expensive thing a hover can do.
   -------------------------------------------------------------------------- */
.m-lift{
  transition:
    translate var(--m-dur-2) var(--m-ease-out),
    box-shadow var(--m-dur-2) var(--m-ease-out),
    border-color var(--m-dur-2) var(--m-ease-out);
}
.m-lift:hover{
  translate:0 calc(var(--m-lift) * -1);
  box-shadow:0 18px 44px -20px rgba(0,0,0,.55);
}

/* --------------------------------------------------------------------------
   Section reveal — opacity + a small translate. Never height, never scale, so
   it cannot shift layout or trigger a reflow of the section below.
   The `.m-reveal` class is ADDED BY JAVASCRIPT, and only to sections that are
   off screen at load time. That ordering is the no-JS guarantee: with scripts
   disabled no element ever receives the class, so nothing can be left hidden —
   and an off-screen section cannot flash, because it is not on screen yet.
   `--m-d` carries a per-item stagger, applied only to small groups.
   -------------------------------------------------------------------------- */
.m-reveal{
  opacity:0; translate:0 var(--m-reveal-y);
  transition:
    opacity var(--m-dur-3) var(--m-ease-out),
    translate var(--m-dur-3) var(--m-ease-out);
  transition-delay:var(--m-d,0ms);
}
.m-reveal.in{ opacity:1; translate:0 0; }

/* --------------------------------------------------------------------------
   Navigation — deliberately NOTHING here.
   An animated underline was added here once and removed on request: the pages
   already mark the active item with a 5 px accent dot
   (`.nav-links a.active::after{ left:50%; transform:translateX(-50%) }`), and
   the underline's `inset-inline:0` silently broke it. In RTL that shorthand
   resolves `inset-inline-end` to `left`, so the dot ended up with BOTH left:50%
   and right:0 — and an absolutely positioned box with start, end AND width set
   is over-constrained, which makes the browser drop `left` in RTL. The dot
   jumped to the right edge. Removing the rule restores the dot centred.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Mobile menu — a symmetric open/close spring with NO JavaScript change.
   The pages toggle a `.open` class (not inline display), so overriding the
   display mechanism with visibility + opacity + translate is safe. The panel is
   position:fixed, so keeping it in flow costs nothing, and `visibility:hidden`
   takes it out of hit-testing and the accessibility tree when closed.
   `step-end` on the closing transition is what lets it animate out and only
   THEN become hidden; without it the panel would vanish before moving.
   -------------------------------------------------------------------------- */
.mobile-panel{
  display:flex; opacity:0; translate:0 -10px;
  pointer-events:none; visibility:hidden;
  transition:
    opacity var(--m-dur-3) var(--m-ease-out),
    translate var(--m-dur-3) var(--m-ease-out),
    visibility var(--m-dur-3) step-end;
}
.mobile-panel.open{
  opacity:1; translate:0 0;
  pointer-events:auto; visibility:visible;
  transition:
    opacity var(--m-dur-3) var(--m-ease-out),
    translate var(--m-dur-3) var(--m-ease-out),
    visibility 0s;
}

/* --------------------------------------------------------------------------
   Responsive fix — the signed-in header row cannot fit, and the overflow
   pushed the menu button off-screen.

   At 390px the row is: brand + theme + lang + account + "Open dashboard" +
   "Subscribe" + menu. Measured, that needs ~340px inside a 262px box, so the
   flex row overflows past the inline-end edge and the hamburger — the last
   item, so the one at that edge — lands at left:-43px: unreachable, and only
   when the user is signed in (logged out the buttons are display:none and it
   fits exactly).

   The site already collapses .nav-links and .nav-search into .mobile-panel at
   this breakpoint; the auth CTAs were the third group that was never
   collapsed. They now live in the panel on all six pages that have one.

   `!important` is required: applyAuthVisibility() writes an inline
   `el.style.display`, which would otherwise beat a stylesheet rule.
   The account icon is deliberately kept — it is small, and for admins
   applyAuthNav() repoints it at /admin/, which is how the admin panel stays
   reachable on mobile.
   -------------------------------------------------------------------------- */
@media (max-width: 980px){
  .nav-right > .btn{ display:none !important; }
}

/* --------------------------------------------------------------------------
   Reduced motion — state changes survive, motion does not.
   Nothing here removes a hover or an open/close STATE; it only removes the
   travel, which is exactly the distinction the motion system is built on.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  :root{ --m-reveal-y:0px; --m-lift:0px; --m-press:1; }
  .btn, .btn-accent, .btn-ghost, .btn-sm,
  .m-follow, .m-follow.m-active,
  .m-lift, .m-spot::after,
  .mobile-panel, .mobile-panel.open{
    transition-duration:1ms !important;
    transition-delay:0ms !important;
  }
  .m-reveal{ opacity:1; translate:none; }
  .m-lift:hover{ translate:none; }
  .btn:active{ scale:1; }
}
