/**
 * Hub v2 — compact game overviews, progressive-enhancement pagination, sticky filters.
 *
 * WHY THIS IS A NEW FILE RATHER THAN AN ADDITION TO state-hub-styles.css
 * WP Rocket writes each minified asset to a FIXED path with a single global ?ver=
 * key, so editing an existing stylesheet does not invalidate its minified copy —
 * state-hub-styles.css on disk had been ahead of what browsers were served since
 * 2026-09-04. A brand-new filename has no stale minified copy to be served, so it
 * ships correctly the first time and every time. See .kiro/steering/wordpress.md
 * "WP Rocket minification silently serves stale assets".
 *
 * Everything here is scoped under .sp-hub-v2, which template-state-hub.php adds to
 * .state-hub only for gated states. That means the other 42 hubs are untouched even
 * though the file is one shared stylesheet, and it gives the specificity needed to
 * beat the v1 rules in state-hub-styles.css without !important.
 */

/* ============================================================================
   SHORTLIST — up to three picks from the CURRENT view

   Replaces the v1 three-column Quick Picks block (Best Value / Best Odds / Top
   Prizes Claimed, three games each). Slots hide themselves when fewer games match,
   and state-hub.js repopulates them from the filtered set — see
   sp_render_shortlist() in sp-hub-cards.php for the full reasoning.
   ============================================================================ */

.state-hub.sp-hub-v2 .sp-picks {
  margin: 0.25rem 0 1rem;
}

.state-hub.sp-hub-v2 .sp-picks-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

.state-hub.sp-hub-v2 .sp-picks-title {
  font-size: 1.05rem;
  font-weight: 700;
  margin: 0;
  color: var(--color-text-primary, #1a1a1a);
}

.state-hub.sp-hub-v2 .sp-picks-scope {
  font-size: 0.82rem;
  color: var(--color-text-secondary, #5B6770);
}

.state-hub.sp-hub-v2 .sp-picks-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.6rem;
}

.state-hub.sp-hub-v2 .sp-pick {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  background: #fff;
  border: 1px solid var(--color-border-light, #e0e0e0);
  border-radius: 10px;
  padding: 0.7rem;
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.state-hub.sp-hub-v2 .sp-pick:hover {
  border-color: #319B42;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
}

.state-hub.sp-hub-v2 .sp-pick[hidden] { display: none; }

/* Slot 1 is the actual recommendation, so it gets the green top edge. The mockup used
   the same device to mark its "Highest ranked" card. */
.state-hub.sp-hub-v2 .sp-pick[data-slot="0"] {
  border-top: 3px solid #319B42;
  padding-top: calc(0.7rem - 2px);
}

.state-hub.sp-hub-v2 .sp-pick-rank {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-secondary, #5B6770);
}

.state-hub.sp-hub-v2 .sp-pick[data-slot="0"] .sp-pick-rank { color: #1e7a34; }

.state-hub.sp-hub-v2 .sp-pick-body {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  min-width: 0;
}

.state-hub.sp-hub-v2 .sp-pick-main {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
  flex: 1;
}

.state-hub.sp-hub-v2 .sp-pick-name {
  font-size: 0.92rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--color-text-primary, #1a1a1a);
  overflow-wrap: anywhere;
}

.state-hub.sp-hub-v2 .sp-pick-sub {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: var(--color-text-secondary, #5B6770);
}

/* The plain-language reason the audit asked for. Derived from the active sort, so it
   states the ordering the visitor asked for rather than an invented claim. */
.state-hub.sp-hub-v2 .sp-pick-reason {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.35;
  color: #1e7a34;
  font-weight: 600;
}

.state-hub.sp-hub-v2 .sp-pick-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.3rem 0.6rem;
}

.state-hub.sp-hub-v2 .sp-pick-stats .gc-stat[hidden] { display: none; }

.state-hub.sp-hub-v2 .sp-pick-details {
  align-self: stretch;
  text-align: center;
  margin-top: 0.1rem;
}

/* The section hides itself entirely rather than showing an empty state — when a price
   filter is active it would be tautological, and when nothing matches the grid's own
   no-results message directly below already says so. */
.state-hub.sp-hub-v2 .sp-picks[hidden] { display: none; }

@media (max-width: 768px) {
  /* One column rather than three squeezed side by side. The picks are the answer to
     "what should I buy", so they get full width on the device where that matters. */
  .state-hub.sp-hub-v2 .sp-picks-grid { grid-template-columns: 1fr; }
  .state-hub.sp-hub-v2 .sp-pick-details { align-self: flex-start; padding: 0.4rem 1rem; }
}

/* ============================================================================
   GRID: one row per game instead of a 3-up card grid
   ============================================================================ */

.state-hub.sp-hub-v2 .tab-content .games-grid {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* Column headings for the row layout. Presentational only — screen readers get the
   per-value <i> labels inside each row, which are the accessible source of truth
   because they survive the mobile reflow where this header is hidden. */
.state-hub.sp-hub-v2 .gc-head {
  display: grid;
  grid-template-columns: 64px minmax(0, 2.4fr) minmax(0, 3fr) auto auto;
  gap: 1rem;
  align-items: center;
  padding: 0.5rem 0.85rem;
  background: #eef4ef;
  border: 1px solid var(--color-border-light, #e0e0e0);
  border-radius: 8px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #4b5563;
}

/* ============================================================================
   ROW
   ============================================================================ */

.state-hub.sp-hub-v2 .game-card.gc {
  display: grid;
  grid-template-columns: 64px minmax(0, 2.4fr) minmax(0, 3fr) auto auto;
  gap: 1rem;
  align-items: center;
  padding: 0.7rem 0.85rem;
  background: #fff;
  border: 1px solid var(--color-border-light, #e0e0e0);
  border-radius: 8px;
  box-shadow: none;
  cursor: pointer;
  transition: background-color 0.15s, border-color 0.15s;
}

/* v1 lifted the whole card on hover. At row density that reads as jitter, so the
   feedback is a flat tint instead. */
.state-hub.sp-hub-v2 .game-card.gc:hover {
  transform: none;
  box-shadow: none;
  background: #f6faf7;
  border-color: #319B42;
}

.state-hub.sp-hub-v2 .game-card.gc:focus-within {
  border-color: #319B42;
  outline: 2px solid rgba(49, 155, 66, 0.35);
  outline-offset: 1px;
}

/* --- ticket thumbnail --- */

.state-hub.sp-hub-v2 .gc-thumb {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: 10px;      /* matches the app's GameCard image radius */
  overflow: hidden;
  background: #e0e0e0;      /* app's loading placeholder colour */
  display: flex;
  align-items: center;
  justify-content: center;
}

.state-hub.sp-hub-v2 .gc-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Branded fallback rather than an empty box, same as TicketImage in the app:
   solid brand blue with the ticket price in white. */
.state-hub.sp-hub-v2 .gc-thumb-fallback {
  width: 100%;
  height: 100%;
  background: #004986;
  color: #fff;
  font-size: 0.95rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* --- identity column --- */

.state-hub.sp-hub-v2 .gc-id {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.state-hub.sp-hub-v2 .gc-name {
  font-weight: 700;
  font-size: 0.95rem;
  line-height: 1.25;
  color: var(--color-text-primary, #1a1a1a);
  /* Long names wrap to a second line rather than truncating: the game name is the
     thing a visitor is scanning for, and Florida has several near-identical ones. */
  overflow-wrap: anywhere;
}

.state-hub.sp-hub-v2 .gc-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: var(--color-text-secondary, #5B6770);
}

.state-hub.sp-hub-v2 .gc-num {
  font-variant-numeric: tabular-nums;
}

.state-hub.sp-hub-v2 .gc-price {
  font-weight: 700;
  color: #004986;
}

.state-hub.sp-hub-v2 .gc-flag {
  font-size: 0.68rem;
  font-weight: 700;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  white-space: nowrap;
}

.state-hub.sp-hub-v2 .gc-flag--claimed {
  background: #fdecea;
  color: #b3261e;
}

.state-hub.sp-hub-v2 .gc-flag--combo {
  background: #e8f5e9;
  color: #166534;
}

/* --- metrics column --- */

.state-hub.sp-hub-v2 .gc-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.5rem;
  min-width: 0;
}

.state-hub.sp-hub-v2 .gc-stat {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.state-hub.sp-hub-v2 .gc-stat b {
  font-size: 0.88rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--color-text-primary, #1a1a1a);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.state-hub.sp-hub-v2 .gc-stat i {
  font-style: normal;
  font-size: 0.68rem;
  line-height: 1.2;
  color: var(--color-text-secondary, #5B6770);
}

.state-hub.sp-hub-v2 .gc-stat b.roi-positive,
.state-hub.sp-hub-v2 .gc-stat b.text-success { color: #1e7a34; }
.state-hub.sp-hub-v2 .gc-stat b.roi-negative,
.state-hub.sp-hub-v2 .gc-stat b.text-error   { color: #b3261e; }

/* --- grade --- */

.state-hub.sp-hub-v2 .game-card.gc .footer-grade {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-shrink: 0;
}

/* --- actions --- */

.state-hub.sp-hub-v2 .gc-act {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-shrink: 0;
}

.state-hub.sp-hub-v2 .gc-details {
  background: #004986;
  color: #fff;
  border: 1px solid #004986;
  border-radius: 8px;
  padding: 0.4rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
  min-height: 36px;
  transition: background-color 0.15s;
}

.state-hub.sp-hub-v2 .gc-details:hover { background: #013a6b; }

.state-hub.sp-hub-v2 .game-card.gc .footer-actions {
  display: flex;
  gap: 0.3rem;
  margin: 0;
  padding: 0;
  border: 0;
}

/* Icon-only at row density. The .sp-card-action-label span is NOT removed from the
   markup — sp-game-actions.js writes into it to flip Save/Saved and Compare/Selected,
   and would throw on a missing node. It is visually hidden and still announced. */
.state-hub.sp-hub-v2 .game-card.gc .sp-card-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  min-height: 36px;
  padding: 0.3rem;
  background: none;
  border: 1px solid #cfd8dc;
  border-radius: 8px;
  cursor: pointer;
  color: #004986;
  font-size: 1rem;
  line-height: 1;
}

.state-hub.sp-hub-v2 .game-card.gc .sp-card-action:hover { border-color: #004986; }
.state-hub.sp-hub-v2 .game-card.gc .sp-card-action.sp-saved,
.state-hub.sp-hub-v2 .game-card.gc .sp-card-action.sp-selected {
  background: #004986;
  color: #fff;
  border-color: #004986;
}

.state-hub.sp-hub-v2 .game-card.gc .sp-card-action-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ============================================================================
   CONTROL SURFACE — one stack, not two competing menus

   Modelled on mobile-app/src/screens/GameBrowserScreen.tsx:

     [ Category ▾ ] [ Sort by ▾ ] [ Price ▾ ]   three equal-width dropdowns
     [ search, full width                    ]
     N games                  ✕ Clear filters

   The desktop tab strip stays (there is room for it and it reads well) and the
   Category dropdown is hidden. Below 769px that inverts. Both are in the DOM at all
   times and drive the same state — state-hub.js syncs them in both directions.
   ============================================================================ */

.state-hub.sp-hub-v2 .filter-bar {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.state-hub.sp-hub-v2 .sp-hub-controls {
  display: flex;
  gap: 0.5rem;
}

/* Sized to land near the app's 40px control height. The label sits above the value
   rather than replacing it (the app can show a fixed "Sort By" caption because its
   control is a button opening a sheet; a native <select> always renders its selected
   option, so the caption has to be its own line to say what the control is for). Keep
   this compact — it was noticeably chunkier at 0.35rem/0.68rem/0.85rem. */
.state-hub.sp-hub-v2 .sp-hub-control {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  background: #fff;
  border: 1px solid var(--color-border-light, #e0e0e0);
  border-radius: 10px;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
}

.state-hub.sp-hub-v2 .sp-hub-control-label {
  font-size: 0.6rem;
  line-height: 1.3;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-secondary, #5B6770);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The select is unstyled and transparent so the whole .sp-hub-control reads as one
   control, which is what makes the label-above-value pattern work. */
.state-hub.sp-hub-v2 .sp-hub-control select {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  padding: 0 0.9rem 0 0;
  margin: 0;
  width: 100%;
  min-width: 0;
  font-size: 0.8rem;
  line-height: 1.4;
  font-weight: 600;
  color: var(--color-text-primary, #1a1a1a);
  cursor: pointer;
  text-overflow: ellipsis;
  /* caret */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235B6770' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.15rem center;
  background-size: 9px 6px;
}

.state-hub.sp-hub-v2 .sp-hub-control select:focus-visible {
  outline: 2px solid #319B42;
  outline-offset: 2px;
}

/* Active = this control is narrowing the list. Blue border + blue label, exactly the
   app's dropdownActive / dropdownTextActive. Sort never gets this — it reorders
   rather than narrows. */
.state-hub.sp-hub-v2 .sp-hub-control.is-active {
  border: 1.5px solid #004986;
  padding: calc(0.2rem - 0.5px) calc(0.5rem - 0.5px);
}

.state-hub.sp-hub-v2 .sp-hub-control.is-active .sp-hub-control-label,
.state-hub.sp-hub-v2 .sp-hub-control.is-active select {
  color: #004986;
}

/* --- search --- */

.state-hub.sp-hub-v2 .sp-hub-search .search-input {
  width: 100%;
  box-sizing: border-box;
  background: #fff;
  border: 1px solid var(--color-border-light, #e0e0e0);
  border-radius: 12px;
  padding: 0.55rem 0.75rem;
  font-size: 0.9rem;
  min-height: 40px;
  color: var(--color-text-primary, #1a1a1a);
}

.state-hub.sp-hub-v2 .sp-hub-search .search-input:focus-visible {
  outline: 2px solid #319B42;
  outline-offset: 1px;
  border-color: #004986;
}

/* --- meta row: count + clear --- */

.state-hub.sp-hub-v2 .sp-hub-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-height: 28px;
}

.state-hub.sp-hub-v2 .sp-hub-meta .results-count {
  font-size: 0.82rem;
  color: var(--color-text-secondary, #5B6770);
}

/* Hidden until something is actually filtered — .visible is added by
   updateResetButtonVisibility(). Styled as the app's "✕ Clear Filters" chip. */
.state-hub.sp-hub-v2 .sp-hub-meta .reset-filters-btn {
  display: none;
  background: #EBF2FA;
  color: #004986;
  border: 0;
  border-radius: 999px;
  padding: 0.3rem 0.7rem;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}

.state-hub.sp-hub-v2 .sp-hub-meta .reset-filters-btn.visible { display: inline-block; }
.state-hub.sp-hub-v2 .sp-hub-meta .reset-filters-btn:hover { background: #dbe8f5; }

/* Desktop: the tab strip is the category control, so hide the dropdown. */
@media (min-width: 769px) {
  .state-hub.sp-hub-v2 .sp-hub-control--category { display: none; }
}

/* Mobile: the tab strip becomes a horizontally-scrolling row that competes with the
   dropdowns for the same job, so drop it and let Category do the work. */
@media (max-width: 768px) {
  .state-hub.sp-hub-v2 .tab-bar { display: none; }
}

/* ============================================================================
   PROGRESSIVE-ENHANCEMENT PAGINATION

   The server renders EVERY eligible game. JS then collapses the list to a batch and
   reveals the controls. Both halves of that are deliberately this way round:

     - .gc-hidden is added BY JS. Without JS nothing is hidden, so a visitor (or a
       crawler) that never runs the script sees the complete list. Hiding by default
       in CSS and revealing with JS would have made the no-JS case show 12 games and
       a dead button.
     - .sp-hub-more is display:none until JS adds .is-ready, because the buttons do
       nothing without JS.
   ============================================================================ */

.state-hub.sp-hub-v2 .game-card.gc-hidden { display: none; }

.state-hub.sp-hub-v2 .sp-hub-more {
  display: none;
  gap: 0.5rem;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  margin: 1rem 0 0.5rem;
}

.state-hub.sp-hub-v2 .sp-hub-more.is-ready { display: flex; }

.state-hub.sp-hub-v2 .sp-hub-more button {
  background: #fff;
  color: #004986;
  border: 1px solid #004986;
  border-radius: 8px;
  padding: 0.55rem 1.1rem;
  font-size: 0.88rem;
  font-weight: 700;
  cursor: pointer;
  min-height: 40px;
}

.state-hub.sp-hub-v2 .sp-hub-more button:hover { background: #f0f6fa; }
.state-hub.sp-hub-v2 .sp-hub-more button[hidden] { display: none; }

.state-hub.sp-hub-v2 .sp-hub-more-count {
  font-size: 0.8rem;
  color: var(--color-text-secondary, #5B6770);
}

/* v1's Load More is replaced by .sp-hub-more. Kept in the DOM so a stale cached
   page still has a working control, but hidden once v2 styles are present. */
.state-hub.sp-hub-v2 .load-more-btn { display: none !important; }

/* ============================================================================
   STICKY FILTERS — released at the end of the catalogue

   position:sticky is scoped by its nearest scrolling ancestor, so putting the bar
   inside .sp-hub-grid-section (tabs + filters + grid, and nothing after it) makes it
   stick through the games and let go exactly when the list ends. No scroll listener,
   no IntersectionObserver, no JS at all.

   Requires that no ancestor sets overflow to anything but visible — that silently
   disables sticky. Checked against state-hub-styles.css on 2026-09-12: the only
   overflow rules are on .game-card-image and a 60px thumb, neither an ancestor here.
   ============================================================================ */

@media (min-width: 769px) {
  .state-hub.sp-hub-v2 .sp-hub-grid-section .filter-bar {
    position: sticky;
    top: 0;
    z-index: 20;
    /* Opaque: rows scroll underneath it. */
    background-color: #f1f4f6;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
  }
}

/* ============================================================================
   MOBILE — the row reflows into a condensed card
   ============================================================================ */

@media (max-width: 768px) {
  .state-hub.sp-hub-v2 .gc-head { display: none; }

  /* Mobile reflow follows the mobile app's GameCard: thumbnail on the left spanning
     the identity and metrics rows, then a footer rail separated by a hairline rule
     holding grade / Details / Save / Compare.
     Reference: mobile-app/src/components/GameCard.tsx (mainRow + footer). */
  .state-hub.sp-hub-v2 .game-card.gc {
    grid-template-columns: 72px minmax(0, 1fr) auto;
    grid-template-areas:
      "thumb id    grade"
      "thumb stats stats"
      "act   act   act";
    gap: 0.5rem 0.75rem;
    padding: 0.75rem;
    align-items: start;
  }

  .state-hub.sp-hub-v2 .gc-thumb {
    grid-area: thumb;
    width: 72px;
    height: 72px;
  }
  .state-hub.sp-hub-v2 .gc-id          { grid-area: id; }
  .state-hub.sp-hub-v2 .game-card.gc .footer-grade { grid-area: grade; align-self: start; }
  .state-hub.sp-hub-v2 .gc-stats       { grid-area: stats; }
  .state-hub.sp-hub-v2 .gc-act         { grid-area: act; }

  /* Two columns so Top prize and Top prizes left pair on the first row, and odds
     and ROI on the second. DOM order in sp-hub-cards.php is what makes that work. */
  .state-hub.sp-hub-v2 .gc-stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.35rem 0.75rem;
  }

  /* Footer rail. Was a full-width Details button, which read as the loudest thing on
     the row and pushed Save/Compare around. Now Details is auto-width on the left and
     the actions sit right, with the app's hairline separator above. */
  .state-hub.sp-hub-v2 .gc-act {
    justify-content: space-between;
    width: 100%;
    margin-top: 0.15rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--color-border-light, #e0e0e0);
  }

  .state-hub.sp-hub-v2 .gc-details {
    flex: 0 0 auto;
    padding: 0.4rem 1rem;
    font-size: 0.82rem;
  }

  .state-hub.sp-hub-v2 .gc-name { font-size: 0.9rem; }

  /* Mobile is where the filter bar being reachable matters most, so it sticks here
     too — but the tab bar is a horizontal scroller on narrow screens, so only the
     filter row is pinned. */
  .state-hub.sp-hub-v2 .sp-hub-grid-section .filter-bar {
    position: sticky;
    top: 0;
    z-index: 20;
    background-color: #f1f4f6;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
  }

  .state-hub.sp-hub-v2 .filter-bar .filter-bar-controls {
    flex-wrap: wrap;
  }
}

@media (prefers-reduced-motion: reduce) {
  .state-hub.sp-hub-v2 .game-card.gc { transition: none; }
}
