/* ==========================================================================
   EzyGamers — motion & depth layer
   --------------------------------------------------------------------------
   Loads LAST, after style.css, games.css, leaderboard.css and responsive.css,
   so everything here is an addition on top of a layout that already works
   without it. Nothing in this file changes where an element sits or how big
   it is; it changes depth, entrance and response to the pointer.

   Two rules kept this from turning into a light show:

     1. Anything that hides content is gated behind an attribute that only
        js/motion.js sets. With JavaScript off, or if the observer never runs,
        every element is simply visible - there is no state in which the page
        renders blank because an animation did not fire.

     2. Every animation here is transform + opacity only, so it runs on the
        compositor. No layout property is animated anywhere in this file.

   The whole layer collapses under prefers-reduced-motion at the bottom.
   ========================================================================== */

:root {
  --reveal-dur: .52s;
  --reveal-shift: 16px;
  --hover-fast: .2s;
  --hover-slow: .34s;
  --lift: cubic-bezier(.16, .84, .44, 1);
}

/* ==========================================================================
   1. Ambient background
   --------------------------------------------------------------------------
   The page sat on a flat #080910. Two very low-opacity colour fields give it
   depth without touching contrast: measured against the body text, the
   brightest point of either field moves the background luminance by under 2%.
   They drift on a slow, long loop, which reads as "alive" rather than as
   animation you can actually catch happening.
   ========================================================================== */
body::before {
  content: "";
  position: fixed;
  inset: -20vmax;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(38vmax 38vmax at 18% 12%, rgba(108, 76, 255, .16), transparent 62%),
    radial-gradient(32vmax 32vmax at 86% 34%, rgba(34, 211, 238, .10), transparent 64%),
    radial-gradient(30vmax 30vmax at 62% 88%, rgba(236, 72, 153, .07), transparent 66%);
  animation: ezy-drift 46s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes ezy-drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(2.5vmax, -2vmax, 0) scale(1.08); }
}

/* A faint grid, fixed to the viewport, so scrolling has a reference plane.
   At 3% white on a near-black ground it is felt more than seen. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(rgba(255, 255, 255, .028) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .028) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(120vmax 80vmax at 50% 0%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(120vmax 80vmax at 50% 0%, #000 20%, transparent 78%);
  /* Being position:fixed is not enough to keep this off the scrolling raster:
     unpromoted, a masked viewport-sized layer was re-rastered on every scroll
     frame and cost more than everything else in this file put together.
     Measured on the homepage, promoting it cut scroll time by a quarter and
     layout by ~40%. The sibling ::before is promoted already by its own
     running animation. */
  transform: translateZ(0);
  will-change: transform;
}

/* ==========================================================================
   2. Page entrance
   --------------------------------------------------------------------------
   One short settle on the shell so the first paint is not a hard cut. The
   header and sidebar are excluded from the scroll-reveal system below (they
   are chrome, not content) and get this instead.
   ========================================================================== */
html.ezy-motion .header { animation: ezy-drop .4s var(--lift) both; }
html.ezy-motion .sidebar { animation: ezy-slide-l .44s var(--lift) both; }

@keyframes ezy-drop   { from { opacity: 0; transform: translateY(-10px); } }
@keyframes ezy-slide-l { from { opacity: 0; transform: translateX(-12px); } }

/* The header is flat against the page until you leave the top, then it lifts
   off it. js/motion.js toggles the class. */
.header {
  transition: box-shadow .25s var(--ease), background .25s var(--ease);
}
.header.is-stuck {
  background: rgba(19, 20, 32, .96);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .5);
}

/* ==========================================================================
   3. Scroll reveal
   --------------------------------------------------------------------------
   js/motion.js tags elements with data-reveal and adds .is-in when they
   enter the viewport. --i is the element's position within its shelf, which
   is what turns a row of cards into a stagger instead of a single flash.

   The attribute is the gate: no attribute, no hiding. See the note at the
   top of this file.
   ========================================================================== */
/* Waiting to be revealed: hidden, but with no transform of its own. */
html.ezy-motion [data-reveal] { opacity: 0; }

/* Revealed. An animation rather than a transition, because a transition needs
   a resting `transform` to undo its entrance offset, and that resting value
   outranks every component's :hover transform - a revealed card would take
   the hover shadow but never actually lift.
   `backwards` applies the from-keyframe during the stagger delay and leaves
   nothing behind once the animation is done, so hover is free afterwards. */
html.ezy-motion [data-reveal].is-in {
  opacity: 1;
  animation: ezy-reveal var(--reveal-dur) var(--lift) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}
html.ezy-motion [data-reveal="fade"].is-in { animation-name: ezy-reveal-fade; }
html.ezy-motion [data-reveal="left"].is-in { animation-name: ezy-reveal-left; }
html.ezy-motion [data-reveal="zoom"].is-in { animation-name: ezy-reveal-zoom; }

@keyframes ezy-reveal      { from { opacity: 0; transform: translateY(var(--reveal-shift)); } }
@keyframes ezy-reveal-fade { from { opacity: 0; } }
@keyframes ezy-reveal-left { from { opacity: 0; transform: translateX(calc(var(--reveal-shift) * -1)); } }
@keyframes ezy-reveal-zoom { from { opacity: 0; transform: translateY(10px) scale(.97); } }

/* ==========================================================================
   4. Section headings
   --------------------------------------------------------------------------
   Every shelf now ends its heading row with the same "See all" affordance,
   so the two shelves that had a button and the eight that had only a chevron
   read as one system.
   ========================================================================== */
.section__see {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, .04);
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 700;
  white-space: nowrap;
  transition:
    color var(--hover-fast) var(--ease),
    background var(--hover-fast) var(--ease),
    border-color var(--hover-fast) var(--ease);
}
.section__see i {
  font-style: normal;
  transition: transform var(--hover-fast) var(--ease);
}
.section__see:hover {
  color: #fff;
  background: rgba(139, 92, 246, .18);
  border-color: rgba(139, 92, 246, .5);
}
.section__see:hover i { transform: translateX(3px); }

/* A shelf with a strapline stacks it under the title, so it takes the same
   inset as the accent bar or the two read as two different left edges. */
.section__title + .section__sub { padding-left: 14px; }

/* The heading itself gets an accent bar that grows in with the shelf. */
.section__title {
  position: relative;
  padding-left: 14px;
}
.section__title::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 4px;
  height: 62%;
  border-radius: 999px;
  background: var(--grad);
  transform: translateY(-50%) scaleY(1);
  transform-origin: center;
  transition: transform .5s var(--lift) .12s;
}
/* Collapsed only while its heading is still waiting to be revealed, so a head
   that motion.js never tagged keeps its bar instead of losing it. */
html.ezy-motion .section__head[data-reveal]:not(.is-in) .section__title::before {
  transform: translateY(-50%) scaleY(0);
}

/* The same mark on the pages that open with a title instead of a shelf, so
   Games, Category, Leaderboard and Profile read as the same product. The
   centred empty states are left alone: a left inset knocks them off centre. */
.page-head h1,
.pf-name {
  position: relative;
  padding-left: 16px;
}
.page-head h1::before,
.pf-name::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 5px;
  height: 64%;
  border-radius: 999px;
  background: var(--grad);
  transform: translateY(-50%);
}

/* ==========================================================================
   5. Rails
   --------------------------------------------------------------------------
   A rail that can scroll now says so: the edge it can scroll towards fades
   out under a soft mask, and the mask disappears at that end of the travel.
   Previously the only clue was an arrow that appeared on hover, which a
   touch device never gets.
   ========================================================================== */
.rail-wrap { --fade: 46px; }

.rail {
  overscroll-behavior-x: contain;   /* no accidental back-navigation swipe */
  scroll-padding-left: 2px;
}

.rail-wrap.has-prev .rail {
  mask-image: linear-gradient(90deg, transparent 0, #000 var(--fade), #000 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--fade), #000 100%);
}
.rail-wrap.has-next .rail {
  mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - var(--fade)), transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - var(--fade)), transparent 100%);
}
.rail-wrap.has-prev.has-next .rail {
  mask-image: linear-gradient(90deg, transparent 0, #000 var(--fade), #000 calc(100% - var(--fade)), transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--fade), #000 calc(100% - var(--fade)), transparent 100%);
}

/* The arrows were hover-only, which meant they were also invisible for the
   first second on a laptop trackpad. They now hold a low resting opacity and
   come fully up on hover. */
.rail-wrap .rail-btn { opacity: .45; }
.rail-wrap:hover .rail-btn,
.rail-btn:focus-visible { opacity: 1; }

/* ==========================================================================
   6. Game cards
   --------------------------------------------------------------------------
   The card already lifted and zoomed. What it did not do was feel like one
   object: the lift, the art zoom and the overlay each ran on their own curve
   and duration. They are unified here at 200-340ms on one easing, and the
   lift gains a coloured rim so the hovered card separates from its shelf on
   a dark ground where a drop shadow alone is nearly invisible.
   ========================================================================== */
.gcard {
  transition:
    transform var(--hover-fast) var(--lift),
    box-shadow var(--hover-fast) var(--lift),
    border-color var(--hover-fast) var(--lift);
}
.gcard:hover,
.gcard:focus-visible {
  transform: translateY(-6px);
  box-shadow:
    0 18px 44px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(139, 92, 246, .55),
    0 10px 40px rgba(108, 76, 255, .3);
}
.gcard:active { transform: translateY(-2px) scale(.99); }

.gcard__img { transition: transform var(--hover-slow) var(--lift), opacity .26s var(--ease); }
.gcard:hover .gcard__img { transform: scale(1.08); }

/* A single pass of light across the art on hover. It runs once per hover and
   is driven by transform, so it costs nothing when idle. */
.gcard__thumb::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, .17) 50%, transparent 62%);
  transform: translateX(-120%);
  opacity: 0;
  transition: transform .62s var(--lift), opacity .2s linear;
}
.gcard:hover .gcard__thumb::after {
  transform: translateX(120%);
  opacity: 1;
}

/* Badges settle in with the card and hold a gentle pulse ring only while the
   card is hovered - a badge that pulses forever on sixty cards at once is the
   "flashing" the brief rules out. */
.gcard__badge {
  transition: transform var(--hover-fast) var(--lift), box-shadow var(--hover-fast) var(--ease);
}
.gcard:hover .gcard__badge {
  transform: translateY(-1px) scale(1.04);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .5);
}

.gcard__title { transition: color var(--hover-fast) var(--ease); }
.gcard:hover .gcard__title { color: #fff; }

/* The body strip picks up a hint of the brand gradient on hover so the whole
   tile responds, not just the image. */
.gcard__body { position: relative; }
.gcard__body::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(108, 76, 255, .16), transparent 70%);
  opacity: 0;
  transition: opacity var(--hover-fast) var(--ease);
  pointer-events: none;
}
.gcard:hover .gcard__body::before { opacity: 1; }
.gcard__body > * { position: relative; }

/* ==========================================================================
   7. Continue playing
   --------------------------------------------------------------------------
   These cards carry real progress from GET /api/games: highest_level and
   best_score for the signed-in player. A signed-out visitor has neither, so
   the strip is simply absent rather than showing an invented percentage.
   ========================================================================== */
.gcard--continue .gcard__resume {
  /* A strip between the art and the title, not an overlay on either. Anything
     floated over the bottom of the thumb would sit on top of the hover
     overlay's facts row, and anything over the body would cover the title -
     giving it its own band means it can never collide with either. */
  display: block;
  padding: 9px 11px 2px;
  background: linear-gradient(180deg, rgba(108, 76, 255, .1), transparent);
  border-top: 1px solid var(--line);
}
.gcard__bar {
  display: block;
  height: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .14);
  overflow: hidden;
}
.gcard__bar i {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--grad);
  transform-origin: left center;
  transform: scaleX(var(--p, 0));
  transition: transform .8s var(--lift) .15s;
}
.gcard__resume-txt {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-top: 6px;
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.gcard__resume-txt b { color: #fff; font-weight: 800; }

/* Continue cards say "Resume", not "Preview" — the affordance is different. */
.gcard--continue .gcard__play { background: linear-gradient(135deg, #22c55e, #15803d); color: #fff; }

/* ==========================================================================
   8. Buttons
   ========================================================================== */
.btn {
  position: relative;
  overflow: hidden;
  transition:
    transform var(--hover-fast) var(--lift),
    box-shadow var(--hover-fast) var(--lift),
    background var(--hover-fast) var(--ease),
    border-color var(--hover-fast) var(--ease),
    opacity var(--hover-fast) var(--ease);
}
.btn > * { position: relative; z-index: 1; }

/* Sheen on the primary action only, once per hover. */
.btn--primary::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 36%, rgba(255, 255, 255, .38) 50%, transparent 64%);
  transform: translateX(-130%);
  transition: transform .6s var(--lift);
}
.btn--primary:hover::after { transform: translateX(130%); }

.btn:active { transform: translateY(1px) scale(.975); }

/* Loading state: the label stays put (so the button does not resize and shove
   its neighbours) and a spinner sits over it. */
.btn.is-loading {
  pointer-events: none;
  color: transparent;
}
.btn.is-loading::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .35);
  border-top-color: #fff;
  animation: ezy-spin .7s linear infinite;
  z-index: 2;
}
.btn--primary.is-loading::before { border-color: rgba(11, 12, 20, .35); border-top-color: #0b0c14; }
@keyframes ezy-spin { to { transform: rotate(360deg); } }

.btn[disabled], .btn.is-disabled { opacity: .5; pointer-events: none; }

.icon-btn { transition: background var(--hover-fast) var(--ease), color var(--hover-fast) var(--ease), transform var(--hover-fast) var(--lift); }
.icon-btn:hover { transform: translateY(-1px); }

/* ==========================================================================
   9. Category cards
   --------------------------------------------------------------------------
   The tint each category already carries in --cat-grad now sweeps across on
   hover and the glyph tilts, so a grid of eight reads as eight destinations
   rather than eight labels.
   ========================================================================== */
.cat-card {
  transition:
    transform var(--hover-fast) var(--lift),
    border-color var(--hover-fast) var(--ease),
    box-shadow var(--hover-fast) var(--lift);
}
.cat-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 16px 36px rgba(0, 0, 0, .5);
}
.cat-card::before { transition: opacity var(--hover-slow) var(--ease), transform var(--hover-slow) var(--lift); }
.cat-card:hover::before { transform: scale(1.14); }
.cat-card__ico {
  transition: transform var(--hover-slow) var(--lift);
  transform-origin: 50% 70%;
}
.cat-card:hover .cat-card__ico { transform: scale(1.16) rotate(-7deg); }
.cat-card--empty:hover .cat-card__ico { transform: none; }

/* ==========================================================================
   10. Hero
   --------------------------------------------------------------------------
   Two soft orbs and a slow shimmer over the existing hero.svg. The hero keeps
   its height; only its depth changes.
   ========================================================================== */
.hero { isolation: isolate; }
.hero__orbs {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  overflow: hidden;
}
.hero__orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(38px);
  opacity: .5;
  will-change: transform;
}
.hero__orb--a {
  width: 260px; height: 260px;
  right: 12%; top: -70px;
  background: radial-gradient(circle, rgba(108, 76, 255, .85), transparent 68%);
  animation: ezy-orb-a 17s ease-in-out infinite alternate;
}
.hero__orb--b {
  width: 200px; height: 200px;
  right: 30%; bottom: -80px;
  background: radial-gradient(circle, rgba(34, 211, 238, .6), transparent 68%);
  animation: ezy-orb-b 21s ease-in-out infinite alternate;
}
@keyframes ezy-orb-a { to { transform: translate3d(-46px, 34px, 0) scale(1.16); } }
@keyframes ezy-orb-b { to { transform: translate3d(38px, -30px, 0) scale(1.1); } }

/* The eyebrow pill breathes once on entry rather than continuously. */
html.ezy-motion .hero__eyebrow { animation: ezy-pop .5s var(--lift) .1s both; }
@keyframes ezy-pop { from { opacity: 0; transform: translateY(8px) scale(.94); } }

/* Featured game visual in the hero: real catalogue art, poster proportion,
   floating gently. Hidden below 1100px where the copy needs the width. */
.hero__art { display: none; }
@media (min-width: 1360px) {
  .hero__art {
    position: relative;
    z-index: 2;
    display: block;
    flex: 0 0 auto;
    width: 150px;
    border-radius: 16px;
    overflow: hidden;
    border: 1px solid var(--line-strong);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .6), 0 0 0 1px rgba(139, 92, 246, .3);
    animation: ezy-float 7.5s ease-in-out infinite alternate;
    transition: transform var(--hover-slow) var(--lift), box-shadow var(--hover-slow) var(--lift);
  }
  .hero__art[hidden] { display: none; }
  .hero__art img { width: 100%; height: 225px; object-fit: cover; }
  .hero__art:hover {
    animation-play-state: paused;
    transform: translateY(-6px) rotate(0deg) scale(1.03);
  }
  .hero__art-cap {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 18px 10px 8px;
    background: linear-gradient(180deg, transparent, rgba(6, 7, 13, .95));
    font-size: 11px;
    font-weight: 800;
    letter-spacing: .04em;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
@keyframes ezy-float {
  from { transform: translateY(-5px) rotate(-2deg); }
  to   { transform: translateY(7px) rotate(2deg); }
}

/* ==========================================================================
   11. Featured carousel
   ========================================================================== */
.fslide.is-active .fslide__body > * { animation: ezy-rise .5s var(--lift) both; }
.fslide.is-active .fslide__cat   { animation-delay: .05s; }
.fslide.is-active .fslide__title { animation-delay: .11s; }
.fslide.is-active .fslide__desc  { animation-delay: .17s; }
.fslide.is-active .fslide__body .btn { animation-delay: .23s; }
@keyframes ezy-rise { from { opacity: 0; transform: translateY(14px); } }

/* Very slow drift on the active slide's art: the still gains life without
   anything moving fast enough to distract from the copy on top of it. */
.fslide.is-active img { animation: ezy-kenburns 14s ease-in-out infinite alternate; }
@keyframes ezy-kenburns { from { transform: scale(1); } to { transform: scale(1.05); } }

.fmain__nav { transition: background var(--hover-fast) var(--ease), transform var(--hover-fast) var(--lift); }
.fmain__nav:hover { transform: scale(1.08); }
.fdot::before { transition: background .2s var(--ease); }
.fdot { transition: width .3s var(--lift); }

/* ==========================================================================
   12. Panels, rows and the rest of the product
   --------------------------------------------------------------------------
   The same language applied to Leaderboard, Profile, Arena and the play page,
   so the site is one product rather than a styled home page in front of five
   plain ones.
   ========================================================================== */
.lb-row { transition: background var(--hover-fast) var(--ease), transform var(--hover-fast) var(--lift), border-color var(--hover-fast) var(--ease); }
.lb-row:hover { transform: translateX(4px); border-color: var(--line-strong); }

.card-panel,
.panel,
.lb-banner,
.toolbar,
.ginfo {
  transition: border-color var(--hover-slow) var(--ease), box-shadow var(--hover-slow) var(--ease);
}
.card-panel:hover,
.panel:hover { border-color: var(--line-strong); }

.filter, .tag, .chip, .lb-seg, .tabs__btn, .tool {
  transition:
    color var(--hover-fast) var(--ease),
    background var(--hover-fast) var(--ease),
    border-color var(--hover-fast) var(--ease),
    transform var(--hover-fast) var(--lift);
}
.filter:hover, .tag:hover, .tool:hover { transform: translateY(-2px); }
.filter:active, .tag:active, .tool:active { transform: translateY(0); }

.mini { transition: background var(--hover-fast) var(--ease), transform var(--hover-fast) var(--lift); }
.mini:hover { transform: translateX(4px); }
.mini img { transition: transform var(--hover-slow) var(--lift); }
.mini:hover img { transform: scale(1.06); }

.sresult { transition: background .15s var(--ease), padding-left .15s var(--ease); }
.sresult:hover, .sresult.is-highlighted { padding-left: 18px; }

.nav__item { transition: background var(--hover-fast) var(--ease), color var(--hover-fast) var(--ease); }
.nav__item .nav__ico { transition: transform var(--hover-fast) var(--lift); }
.nav__item:hover .nav__ico { transform: scale(1.14); }

/* The active nav item gains a rail on its leading edge, which is legible at a
   glance in a list of fifteen in a way a background tint alone is not. */
.nav__item.is-active { position: relative; }
.nav__item.is-active::before {
  content: "";
  position: absolute;
  left: -6px;
  top: 22%;
  bottom: 22%;
  width: 3px;
  border-radius: 999px;
  background: var(--grad);
}

.footer a { transition: color .15s var(--ease), transform .15s var(--ease); }
.footer a:hover { transform: translateX(3px); }

/* ==========================================================================
   13. Skeletons
   --------------------------------------------------------------------------
   Loading placeholders must never be revealed *and* swept — they are already
   an animation. They opt out of the reveal system in js/motion.js; this is
   the belt-and-braces half.
   ========================================================================== */
html.ezy-motion .gcard--skeleton[data-reveal],
html.ezy-motion .lb-row--skel[data-reveal] {
  opacity: 1;
  animation: none;
}
.gcard--skeleton:hover { transform: none; box-shadow: none; border-color: var(--line); }
.gcard--skeleton .gcard__thumb::after { display: none; }

/* ==========================================================================
   14. Responsive
   ========================================================================== */
@media (max-width: 1050px) {
  .hero__orb--a { width: 190px; height: 190px; }
  .hero__orb--b { display: none; }
}

@media (max-width: 760px) {
  /* A shorter travel: on a phone the card is most of the viewport, and 16px
     of rise on something that large reads as a jolt. */
  :root { --reveal-shift: 10px; --reveal-dur: .42s; }
  .rail-wrap { --fade: 28px; }
  .section__see { padding: 6px 11px; font-size: 12px; }
  .section__title,
  .section__title + .section__sub { padding-left: 11px; }
  body::after { background-size: 54px 54px; }
}

/* Coarse pointers get no hover choreography — there is no hover to key it to,
   and leaving it in means a tap latches the hovered state until you tap
   elsewhere. */
@media (hover: none) {
  .gcard__thumb::after,
  .btn--primary::after { display: none; }
  .gcard:hover,
  .cat-card:hover,
  .lb-row:hover,
  .mini:hover,
  .filter:hover,
  .tag:hover,
  .tool:hover { transform: none; }
  .gcard:hover .gcard__img { transform: none; }
  .gcard:active { transform: scale(.985); }
  .rail-wrap .rail-btn { display: none; }
}

/* ==========================================================================
   15. Motion opt-out
   --------------------------------------------------------------------------
   Everything above collapses. Content still arrives — the reveal system
   resolves to "already visible" rather than "never animates in", which is the
   difference between respecting the preference and hiding the page from
   people who set it.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; }

  html.ezy-motion .header,
  html.ezy-motion .sidebar,
  html.ezy-motion .hero__eyebrow { animation: none; }

  html.ezy-motion [data-reveal],
  html.ezy-motion [data-reveal].is-in {
    opacity: 1;
    animation: none;
  }

  .section__title::before { transform: translateY(-50%) scaleY(1) !important; }

  .hero__orb,
  .fslide.is-active img,
  .fslide.is-active .fslide__body > * { animation: none; }

  .hero__art { animation: none; }

  .gcard__thumb::after,
  .btn--primary::after { display: none; }

  .gcard,
  .gcard__img,
  .cat-card,
  .cat-card__ico,
  .lb-row,
  .mini,
  .btn,
  .icon-btn,
  .filter,
  .tag,
  .tool,
  .footer a,
  .nav__item .nav__ico { transition: none; }

  .gcard:hover,
  .gcard:focus-visible,
  .gcard:active,
  .cat-card:hover,
  .lb-row:hover,
  .mini:hover,
  .btn:hover,
  .btn:active,
  .icon-btn:hover,
  .filter:hover,
  .tag:hover,
  .tool:hover,
  .footer a:hover,
  .fmain__nav:hover { transform: none; }

  .gcard:hover .gcard__img,
  .cat-card:hover .cat-card__ico,
  .cat-card:hover::before,
  .mini:hover img { transform: none; }

  .gcard__bar i { transition: none; }
}
