/* ui.css — the shared interaction layer.
 *
 * Loaded AFTER app.css so it wins on equal specificity. Four jobs, in the order
 * they matter:
 *
 *   1. Motion tokens. app.css had ~30 freehand durations, which is ~30 slightly
 *      different personalities. One set of tokens means a new control inherits
 *      the feel for free.
 *   2. Focus. Before this file there were ZERO :focus-visible rules in app.css,
 *      which styles every menu control. Hover is the one state a phone does not
 *      have and a keyboard player never triggers, so the most-designed state was
 *      the least-reachable one.
 *   3. Press. There was ONE :active rule in the whole client. A control that
 *      only changes colour on hover has no moment of commitment, and on touch it
 *      has no feedback at all until the result arrives.
 *   4. Reduced motion. 15 infinite animations, 3 of them covered. The other 12
 *      kept running for someone who had asked them not to.
 *
 * Rules for this file live in docs/ui-rules.xml and are enforced by
 * tools/checks/ui-rules.mjs.
 */

/* ===================== 0. `hidden` has to mean hidden ======================
   The UA stylesheet says [hidden] { display: none }, and ANY author rule that
   sets `display` beats it, because a class selector outranks the UA origin.
   app.css:1367 does exactly that: `.shop-block.panelled { display: flex }`.

   So a Shop section the server had marked hidden, because there is nothing in
   it this week, kept rendering as an empty bordered panel. It was found by
   measuring the Shop at 320px, where two hidden sections were still spending
   140px between them, but it was never a small-screen bug: those empty boxes
   were on every device.

   !important is the right tool here and is what the resets use. `hidden` is not
   a style, it is a statement that the element is not part of the page yet. */
[hidden] { display: none !important; }

/* ===================== 1. Motion tokens =====================================
   The register is an .io game, so the personality is fast and physical, never
   floaty. Frequency governs duration: the more often a control is used, the
   shorter its motion gets, because nobody should ever wait on an animation they
   trigger a hundred times a session. */
:root {
  --t-instant: 90ms;      /* press feedback, the thing that must beat perception */
  --t-quick: 130ms;       /* hover, focus, toggles: used constantly, so kept short */
  --t-base: 200ms;        /* view and page changes */
  --t-expressive: 320ms;  /* once-a-session moments that earn a real gesture */

  --e-enter: cubic-bezier(0.16, 1, 0.3, 1);   /* arriving decelerates */
  --e-exit: cubic-bezier(0.7, 0, 0.84, 0);    /* leaving accelerates */
  --e-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --e-snap: cubic-bezier(0.34, 1.4, 0.64, 1); /* a little overshoot, used sparingly */

  --stagger: 34ms;

  /* Cyan, the blob's own colour. ~13:1 against the app background, and the 2px
     offset keeps it readable on the gold and violet buttons too. */
  --focus: #7fe3ff;
}

/* ===================== 2. Focus =============================================
   One rule, every control, including ones added later. :focus-visible means a
   mouse click does not paint a ring but a Tab key does, which is the behaviour
   people expect from a native control.

   `outline` rather than `box-shadow` on purpose: outline follows border-radius
   in every current browser, costs no layout, and cannot be clipped by the
   element's own overflow: hidden (which .skin-card.shimmer sets). */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  /* Nothing else changes. A focus ring that also moves the element makes the
     keyboard path feel different from the mouse path for no reason. */
}

/* The ring needs somewhere to sit. These clip their own edge, so an offset ring
   would be trimmed by the parent rather than drawn around the element. */
#navRail, .skins-grid { overflow: visible; }

/* Inputs get a visible resting border too. A field that is invisible until you
   click it is a field nobody knows is there. */
#name, #joinCode, #authEmail, #claimCode, #refInput, #refAlias {
  border: 1px solid rgba(255,255,255,0.16);
  background: rgba(255,255,255,0.96);
  transition: border-color var(--t-quick) var(--e-standard),
              box-shadow var(--t-quick) var(--e-standard);
}
#name:focus-visible, #joinCode:focus-visible {
  outline-offset: 0; /* these sit on a dark stage, so the ring reads better tight */
}

/* ===================== 3. Press =============================================
   The commitment beat. Landing inside 100ms is the requirement, hence
   --t-instant. Scale rather than translate, because a press should read as the
   control taking the force, not as it moving somewhere. */
:where(.chip, .primary, button.small, .nav-item, .donate-chip, .title-chip,
       .loot-tab, .loot-buy, .loot-open, .offer-btn, .bundle-btn, .sp-buy,
       .sp-cancel, .back-link, .btn-signout, .play-friends-btn, .shop-nav,
       .spot-arrow, .loot-nav, .gem-pack, .tb-brand, .pager-arrow,
       .pager-dot):active:not(:disabled) {
  transform: scale(0.965);
  transition-duration: var(--t-instant);
}

/* Cards are big, so the same 0.965 reads as a lurch. Less travel, same idea. */
:where(.skin-card, .loot-crate-card, .alpha-item, .how-card):active:not(:disabled) {
  transform: scale(0.988);
  transition-duration: var(--t-instant);
}

/* The primary action earns a brighter press, not just a smaller one. */
.primary:active:not(:disabled),
.play-big:active:not(:disabled) { filter: brightness(1.12); }

/* Disabled controls say so by not responding at all. */
:where(button, .chip, .skin-card):disabled { cursor: not-allowed; }
:where(button, .chip):disabled:active { transform: none; filter: none; }

/* Copy must never name an input this device does not have. The in-game HUD told
   phone players their dash was "Space / Shift", which is two keys they do not
   have, on the one screen where they most need to know what to press. These were
   scoped to #howPanel; they are general now, because the rule is. */
.mobile-only { display: none; }
body.touch-device .mobile-only { display: revert; }
body.touch-device .desktop-only { display: none; }

/* Hover belongs to devices that have a pointer. Without this, a tap on a phone
   leaves the hover state stuck on until you tap somewhere else, so the last
   thing you touched looks permanently selected. */
@media (hover: none) {
  :where(.chip, .skin-card, .loot-crate-card, .gem-pack, .title-chip,
         .donate-chip, .alpha-item, .loot-tab):hover {
    transform: none;
    box-shadow: none;
  }
}

/* ===================== 4. Reduced motion ====================================
   The floor is prefers-reduced-motion, but the rule this codebase follows is
   stronger: the reduced path loses INTENSITY only. It never loses information.

   That distinction matters most here, because these auras are not decoration.
   A pulsing green glow is how a player reads "this skin is the 0.5% tier". Turn
   the animation off and you have to leave the glow behind at a readable
   strength, or the reduced-motion player is shopping with the price tags off. */
@media (prefers-reduced-motion: reduce) {

  /* Rarity flair: held at the animation's own mid-point, so the tier still
     reads at a glance. Values lifted from the 50% keyframe of each loop. */
  .fx-aura {
    animation: none;
    box-shadow: inset 0 0 0 1.5px rgba(255,255,255,0.45), 0 0 11px 3px var(--fx, #4ade80);
  }
  .fx-horizon {
    animation: none;
    box-shadow: inset 0 0 0 1.5px rgba(255,235,215,0.5), 0 0 11px 3px rgba(255,217,184,0.45),
                0 0 21px 6px rgba(201,106,106,0.30);
  }
  .fx-bastet {
    animation: none;
    box-shadow: inset 0 0 0 1.5px rgba(255,255,255,0.45), 0 0 12px 3px rgba(240,199,90,0.5),
                0 0 6px 2px rgba(245,242,234,0.35);
  }
  /* Sparkles stay put and stay lit rather than twinkling. Still legible as
     "this one is special", with nothing blinking. */
  .fx-sparkle::before, .alpha-swatch::before { animation: none; opacity: 0.85; }
  /* The comet ring keeps its ring and its comet, and stops orbiting. */
  .fx-ring { animation: none; }
  /* Sheens are pure decoration: no information is carried, so they go quiet. */
  .fx-shine::after, .alpha-swatch::after { animation: none; opacity: 0.25; }
  .alpha-swatch {
    animation: none;
    box-shadow: 0 2px 10px rgba(0,0,0,0.35), inset 0 3px 6px rgba(255,255,255,0.15),
                inset 0 -3px 8px rgba(0,0,0,0.30), 0 0 11px 2px var(--glow, #fff);
  }
  .thanks-orb {
    animation: none;
    box-shadow: 0 0 40px rgba(150,100,255,0.6), inset 0 0 20px rgba(0,0,0,0.5);
  }
  /* The skull bobs to say "this screen is alive, not frozen". A static skull
     says the same thing without the movement. */
  #death .skull { animation: none; }
  /* Entrances become fades. The meaning (something new arrived) survives; the
     travel does not. */
  .thanks-card { animation-name: rmFade; animation-duration: var(--t-base); }
  @keyframes rmFade { from { opacity: 0; } to { opacity: 1; } }
  /* The prestige card is not an entrance, it is a whole life: arrive, be read,
     leave. It used to share the line above, and rmFade ends at opacity 1 while
     app.css:916 sets `forwards` in its shorthand — so with Reduce Motion on, the
     card held at full opacity for ever, dead centre of the screen, for the rest
     of the match. Reduced motion is meant to cost intensity, never a feature, and
     losing the exit lost one. Same 2.6s life and the same reading time; only the
     travel and the scale go. */
  #prestige.show { animation-name: rmPrestige; animation-duration: 2600ms; }
  @keyframes rmPrestige {
    0% { opacity: 0; } 8% { opacity: 1; } 82% { opacity: 1; } 100% { opacity: 0; }
  }
  /* The tier star spins in from -140deg. Small element, but still rotation, so it
     arrives already pointing the right way. */
  #prestige .pr-star { animation: none; }
  /* Nothing in the shell should transition further than a fade. */
  .pager-track { transition: none; }
}

/* ===================== 5. The pager =========================================
   The replacement for every overflow-y: auto in the shell. See
   public/js/pager.js for the measuring, and docs/ui-rules.xml for why scrolling
   is not an option here.

   Deliberately almost styleless. The paged container keeps its OWN display,
   because it is usually a grid that other rules already position (.shop-grid > *
   in mobile.css, for one), and wrapping it or changing its display would break
   them. All this layer does is hide the children that are not on this page and
   give the bar a look. Hiding rather than translating is what lets a CSS grid
   reflow the survivors into full rows, so there is never a half-visible card. */
.pager-page-hidden { display: none !important; }

/* The page bar. Only rendered when there is more than one page: a single dot is
   a control that cannot do anything, which is worse than no control. */
.pager-bar {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 4px; max-width: 100%; padding-top: 2px;
}
.pager-arrow {
  display: flex; align-items: center; justify-content: center;
  /* 44px is the accessibility floor for a touch target. The glyph is small; the
     hit area is not. */
  min-width: 44px; min-height: 44px; padding: 0;
  font: inherit; font-size: 19px; line-height: 1; cursor: pointer;
  color: #9aa3c7; background: none; border: none; border-radius: 12px;
  transition: color var(--t-quick) var(--e-standard),
              background var(--t-quick) var(--e-standard),
              transform var(--t-instant) var(--e-standard);
}
.pager-arrow:hover:not(:disabled) { color: #eaf1ff; background: rgba(255,255,255,0.06); }
.pager-arrow:disabled { opacity: 0.22; cursor: default; }

.pager-dots { display: flex; align-items: center; gap: 4px; }
.pager-dot {
  /* Same story: a 9px dot inside a 44px target. */
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 44px; padding: 0;
  background: none; border: none; cursor: pointer; border-radius: 8px;
  transition: transform var(--t-instant) var(--e-standard);
}
.pager-dot::before {
  content: ""; width: 9px; height: 9px; border-radius: 50%;
  background: rgba(255,255,255,0.20);
  transition: background var(--t-quick) var(--e-standard),
              transform var(--t-quick) var(--e-snap);
}
.pager-dot:hover::before { background: rgba(255,255,255,0.42); }
.pager-dot[aria-selected="true"]::before { background: #5c9bff; transform: scale(1.35); }

/* The count is the honest part: dots say "there is more", this says how much
   more. Tabular figures so it does not reflow as the number changes. */
.pager-count {
  font-size: 12px; font-weight: 700; color: #7f88ad; margin-left: 4px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Page changes slide the width of a hair, not the width of the screen. Large
   travel across a big surface is what makes people queasy, and this is a
   control someone will use dozens of times in a session. */
.pager-track.pager-turn { animation: pagerTurn var(--t-base) var(--e-enter); }
@keyframes pagerTurn {
  from { opacity: 0; transform: translateX(var(--pager-from, 10px)); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .pager-track.pager-turn { animation: rmFade var(--t-quick); }
}

/* Full-width tab views fill the stage from the top instead of floating in the
   middle of it. #stage centres its child vertically, which is right for the Play
   hero and wrong for a page of cards: on a phone it left about 120px of empty
   space above the heading and then paged the content that would have fitted in
   it. Panels and the hero still centre; only the full views stretch. */
.view.full { align-self: stretch; }

/* How to Play: the mobile hint and the cursor-lock setting now live INSIDE the
   card grid so the pager treats them as content rather than as chrome it has to
   work around. Full-width rows, so the desktop layout reads as before. */
.how-grid > .how-mobile, .how-grid > .how-setting { grid-column: 1 / -1; margin-top: 0; }

/* ===================== 6. Empty states ======================================
   A dead end is a defect. Each of these already said what happened and what it
   meant, and then stopped: "Nothing in your locker yet" is true, and leaves a
   new player looking at a blank panel with no idea that the Shop is one click
   away. Every one now ends in a real button (see the data-goto handler in
   game.js), and they share a look so the fourth one a player meets is already
   familiar.

   Dashed rather than solid on purpose: it reads as a space waiting to be
   filled, not as a panel that failed to load. */
.locker-empty, .mail-empty, .shop-empty {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 26px 18px; text-align: center; opacity: 1;
  border: 1px dashed rgba(255,255,255,0.13); border-radius: 14px;
  background: rgba(255,255,255,0.015);
}
.es-mark { font-size: 30px; line-height: 1; margin-bottom: 2px; }
.es-line { font-size: 15px; font-weight: 700; color: #eaf1ff; margin: 0; }
.es-sub { font-size: 12.5px; color: #8ea0d0; margin: 0; max-width: 340px; line-height: 1.5; }
.es-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 9px; }
.acc-stat-empty { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; opacity: 1; color: #8ea0d0; }

/* The "Get Gems" button appended to an insufficient-funds message. Was 200
   characters of inline style on a button that was never wired up. */
.cta-gems {
  margin-left: 5px; padding: 6px 12px; min-height: 32px;
  font: inherit; font-size: 12px; font-weight: 800; cursor: pointer;
  color: #fff; border: none; border-radius: 8px;
  background: linear-gradient(180deg, #4a86e8, #3565bd);
  transition: filter var(--t-quick) var(--e-standard), transform var(--t-instant) var(--e-standard);
}
.cta-gems:hover { filter: brightness(1.1); }

/* ---- The 44px floor, for the controls that were under it -------------------
   docs/ui-rules.xml #touch-targets: "Anything tappable is at least 44 by 44 CSS
   pixels, including the page control." Measured against the running game, these
   were not, and every one of them is on a path that matters: choosing what kind
   of feedback you are sending, choosing what to tip, dismissing the thank-you,
   leaving the Loot Bubble screen, turning its wheel, and the email field on the
   one screen that signs you in.

   They live here rather than beside their own rules because app.css is full (it
   sits exactly on its budget), and because a floor that applies to six unrelated
   components is a rule about the product, not a detail of any one of them. Height
   only: none of these needs to get wider, and widening the tip chips would rewrap
   the row they sit in. */
.fb-kind, .donate-chip, #thanksClose, #lootBackBtn, #authEmail, #fbContact { min-height: 44px; }
/* #donateBtn is deliberately NOT in that list, and this is the note so nobody
   "fixes" it again. It measures 432x35: nine pixels under on the axis that
   matters least, on a control wide enough to hit without looking. Adding the
   nine pushed div.donate-fine 17px past the panel's clipped edge at 740x360,
   i.e. it traded a comfortable target for the refund terms going off the screen
   before money moves. If it is to be 44, the height has to come from the panel
   first. */
.loot-nav { min-width: 44px; min-height: 44px; }
