/* ===== GRID AREA ===== */
.grid-area {
  flex: 1;
  padding: 24px 20px;
  /* REAL BUG, found and fixed: overflow-y: auto with no overflow-x set means the CSS spec forces
     overflow-x to compute as 'auto' too (a non-'visible' axis forces the other away from
     'visible') — this was a real scrollable-in-both-directions container, not just visually
     clipped, and touch-action: pan-y (base.css) only blocks touch PANNING, not genuine layout
     overflow. The .rs-logo-wrap/.nyt-logo-wrap/.steam-logo-wrap tooltip (its ::after, above) is
     220px wide and centered under a logo that's flush against the right edge via margin-left:
     auto — invisible (opacity:0/visibility:hidden) at rest, but visibility:hidden still generates
     a box and still counts toward this container's scrollable overflow, so on a curated category
     page with one of those logos (Book, Musician, Game, Movie) the tooltip alone was enough to
     make the whole content area horizontally draggable on mobile, reported live ("the screen
     moves left and right... the container isn't static horizontally") even while scrolled well
     past the title row it lives in. overflow-x: hidden here is a safe hard backstop, not just a
     tooltip-specific fix: every horizontally-scrolling row nested inside this container (the Top
     100 carousels) scrolls via its own dedicated .dash-carousel-strip overflow-x: auto, which
     keeps working fine nested inside an ancestor that's hidden on that axis. */
  overflow-y: auto;
  overflow-x: hidden;
  /* REAL BUG, found and fixed: -webkit-overflow-scrolling: touch was added here to try to smooth
     out mobile scroll feel, but this legacy property has a long-documented WebKit quirk on nested
     scroll containers (this one, inside body's own overflow:hidden) — after scrolling all the way
     to the bottom (and briefly rubber-band overscrolling past it), a new touch often doesn't
     re-engage the scroll cleanly, making it hard to drag back up (reported live: "it scrolls fine
     till i hit the bottom of the page and then... it's hard to scroll back up"). Modern iOS Safari
     (13+) already gives plain overflow:auto elements native momentum scrolling without this
     property, so it wasn't buying anything — removed outright rather than chasing a workaround for
     a legacy property most teams now recommend dropping entirely on current iOS versions. */
  align-self: stretch;
  /* REAL BUG, found and fixed: a flex item's default min-width is 'auto' (its content's own
     min-content size), not 0 — a descendant with a wide min-content demand (the category
     folder-picker grid's bare `1fr` columns, below — a classic CSS Grid gotcha) could force
     .grid-area ITSELF wider than its flex:1 allocation to accommodate it, pushing the whole
     .main-layout/body wider than the viewport and making the page horizontally scrollable —
     reported live on mobile, screenshot confirmed a sliver of the sidebar peeking in from the
     left edge. overflow-x: hidden above only clips a child's overflow *within* .grid-area's own
     box; it does nothing once .grid-area's own box has already grown too wide. min-width: 0 here
     is the same "hard backstop, not just a fix for one specific cause" philosophy as that
     overflow-x: hidden rule — it rules out this entire class of bug regardless of what inside
     .grid-area might someday have an oversized min-content demand, not just today's cause. */
  min-width: 0;
}

.grid-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
}

/* Wraps #musicgenre-select + #sort-select (index.html) — margin-left: auto lives here, on the
   pair as a group, rather than on .sort-select alone, so the pair pushes right together
   regardless of whether the genre dropdown is currently shown (Music landing page drill-in only)
   or hidden (every other page, where this wrapper just collapses to fit #sort-select alone). */
.grid-header-right {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-left: auto;
}

/* Purple fill / white text, per direct request — overrides the shared .sort-select class it also
   carries (index.html) via this ID selector's higher specificity. The dropdown arrow is baked
   into .sort-select's own background-image as a data URI (base.css) with a fixed gray stroke —
   re-declared here with a white stroke so it's actually visible against the purple fill. */
#musicgenre-select {
  background-color: var(--primary);
  border-color: var(--primary);
  color: #fff;
  font-size: 15px;
  font-weight: 700;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.5'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
#musicgenre-select option {
  color: var(--text-primary);
  background: var(--surface);
}

/* ===== Music landing page (state.view === 'Musician', renderGrid.js's renderMusicGenreLanding())
   — a fixed grid of genre buckets (14 as of Metal folding into Rock, see MUSIC_GENRE_BUCKETS,
   state.js) instead of a flat item list. Modeled loosely on the Add
   modal's .step1-category-tile (icon+label tile shape, addEditModal.css) plus a .sidebar-count-
   style count badge (sidebar.css) — the two closest existing pieces, since no single component
   already combined icon+label+count in a clickable grid card. ===== */
.musicgenre-landing-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  /* More space between rows than between columns, per direct request. 30 -> 50 per a further
     direct follow-up ("increase the vertical padding by twenty pixels" — clarified as "the
     padding between the cards", i.e. this row-gap, not the cards' own internal padding). */
  row-gap: 50px;
  column-gap: 14px;
}
/* 20px above the first row and below the last row only, desktop only, per direct follow-up/
   correction ("this twenty pixels of padding should just be below the top and bottom") — not
   extra gap between every card (that was tried first and corrected). */
@media (min-width: 481px) {
  .musicgenre-landing-grid {
    padding-top: 20px;
    padding-bottom: 20px;
  }
}
.musicgenre-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 22px 10px;
  border: 1.5px solid var(--primary);
  border-radius: 12px;
  /* Purple fill per direct request — same "filled purple, flip to white on hover" swap already
     used elsewhere (e.g. the modal header Save button, the Add-item search icon). */
  background: var(--primary);
  cursor: pointer;
  text-align: center;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
  /* Half the width of its grid column (grid items stretch to fill their track by default —
     justify-items: stretch — so this needs both an explicit width and justify-self: center to
     actually narrow the card rather than just its content), +30px total per two direct
     follow-ups (+15px, then +15px again). */
  width: calc(50% + 30px);
  justify-self: center;
}
.musicgenre-card:hover {
  background: #fff;
  color: var(--primary);
}
.musicgenre-card:hover .musicgenre-card-name { color: var(--primary); }
.musicgenre-card:hover .musicgenre-card-count { color: var(--primary); opacity: 0.75; }
.musicgenre-card-icon {
  font-size: 26px;
  line-height: 1;
}
.musicgenre-card-name {
  font-size: 15px;
  font-weight: 600;
  color: #fff;
}
.musicgenre-card-count {
  font-size: 13px;
  color: rgba(255,255,255,0.8);
}

/* Hover callout listing which saved musicians landed in this bucket, per direct request — same
   visual language as the detail modal's "Why VoteCraft Recommends" tooltip (.vc-sponsored-tooltip,
   detailModal.css): purple fill, small bold uppercase title, an upward-pointing arrow tail back at
   the card that triggered it. Opens downward (below the card) rather than upward since these cards
   sit at the very top of the page with no room above the first row. pointer-events: none — purely
   informational, shouldn't intercept the click a user meant for the card underneath it. */
.musicgenre-tooltip {
  position: absolute;
  top: 100%;
  left: 50%;
  margin-top: 12px;
  width: 260px;
  max-width: 85vw;
  box-sizing: border-box;
  background: var(--primary);
  border: 1px solid var(--primary);
  border-radius: 10px;
  padding: 12px 14px;
  text-align: left;
  white-space: normal;
  box-shadow: 0 4px 16px rgba(0,0,0,0.35);
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, -4px);
  transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
  pointer-events: none;
  z-index: 5;
}

.musicgenre-card:hover .musicgenre-tooltip {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
}

.musicgenre-tooltip::before,
.musicgenre-tooltip::after {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 50%;
  border-style: solid;
  border-color: transparent;
}

.musicgenre-tooltip::before {
  border-width: 14px;
  margin-left: -14px;
  border-bottom-color: var(--primary);
}

.musicgenre-tooltip::after {
  border-width: 13px;
  margin-left: -13px;
  margin-bottom: -1px;
  border-bottom-color: var(--primary);
}

.musicgenre-tooltip-title {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff;
  margin-bottom: 5px;
}

.musicgenre-tooltip-text {
  display: block;
  font-size: 14px;
  font-weight: 400;
  color: rgba(255,255,255,0.85);
  line-height: 1.5;
}
/* Cards 10% larger, desktop only, per direct follow-up (an earlier 20% attempt was placed before
   the base .musicgenre-card rule above, so — same specificity, earlier source order — it was
   silently overridden by it and never actually took effect; this one comes after, so it wins).
   Font size deliberately left unscaled here, per direct request — only width/padding grow.
   1.1 -> 1.2 per a further direct follow-up ("another ten percent larger"), still no font-size
   change. */
@media (min-width: 481px) {
  .musicgenre-card {
    width: calc((50% + 30px) * 1.2);
    padding: 26px 12px;
  }
}

/* ===== Category folder-picker landing (state.view === <category>, renderGrid.js's
   renderCategoryFolderLanding()) — every top-level category tab except Musician/Music Album (both
   stay their own thing, per direct request) now shows its real subfolders as a picker grid
   instead of the primary-folder+unfoldered item list directly. Same card-grid mechanics as
   .musicgenre-card above, but deliberately a DIFFERENT look — purple-outlined instead of
   solid-fill, per direct request, so this doesn't read as a second Music-style page. ===== */
/* Back to a single row of 4, per direct follow-up ("put the 4 folders back into a row") — reverses
   the earlier 2x2 pass. Capped/centered so 4 cards on a wide desktop screen don't stretch
   edge-to-edge into oversized tiles. */
.category-folder-landing-grid {
  display: grid;
  /* minmax(0, 1fr), not bare 1fr — REAL BUG, found and fixed: a bare `1fr` track still respects
     its items' own min-content size as a floor (a folder name like "Publications"/"Board Games"
     refusing to wrap below its own text width), which could force this whole grid — and, per the
     .grid-area min-width:0 fix above, the whole page — wider than the viewport on mobile.
     minmax(0, 1fr) is the standard fix: explicitly allows the track to shrink past its content's
     natural width instead of using it as a floor. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* REAL BUG, found and fixed: grid items default to align-items: stretch — a row's height is
     the tallest item's natural (aspect-ratio-preferred) height, but every item in that row then
     gets STRETCHED to that same height, overriding their own square aspect-ratio in the process.
     One card with a longer wrapped name (e.g. "Publications") could pull its whole row taller,
     turning every card in it into a rectangle instead of a square (reported live on an iPhone,
     screenshot confirmed). align-items: start opts every card out of that stretch — each card
     sizes to its own aspect-ratio-preferred square instead of matching its row's tallest sibling;
     grid-auto-rows: min-content backs that up by sizing rows to their content's minimum instead
     of leaving any ambiguity for auto-sizing to fill via stretch. */
  align-items: start;
  grid-auto-rows: min-content;
  /* 20px -> 30px, per direct follow-up. */
  gap: 30px;
  /* width:90% (not just a smaller max-width) so this actually shrinks on mobile too — mobile's own
     viewport can be narrower than a fixed cap, which alone would only affect desktop. Icon/text
     sizes on the cards themselves are untouched; each square just shrinks with the grid column
     width (aspect-ratio: 1/1). */
  width: 90%;
  max-width: 800px;
  margin: 0 auto;
  /* Explicit, not just relying on the auto margins above — this is now a flex item inside
     .category-landing-page (display:flex; flex-direction:column;), and align-self is the
     unambiguous way to center a flex item along its container's cross axis, per direct follow-up
     ("center the folder squares horizontally... within the page area" — they were reading as
     off-center against the full width of .grid-area, the actual page content pane). */
  align-self: center;
  /* Moves just the folder cards up, not the whole page/carousel — per direct correction ("i only
     want the folder cards moved up"). A margin-top on .category-landing-page itself (tried first)
     shifted the whole vertically-centered group as one block, so the carousel/caption moved too;
     transform doesn't affect layout at all (siblings render exactly where they normally would),
     it only visually offsets this one element. -30px, then another -20px per a further direct
     follow-up scoped to desktop only (-50px total) — mobile stays its own separate -5px (below). */
  transform: translateY(-50px);
}
@media (min-width: 481px) {
  .category-folder-landing-grid {
    padding-top: 20px;
    padding-bottom: 20px;
  }
}
/* Mobile keeps the 2x2 layout even though desktop went back to a single row of 4, per direct
   follow-up ("on mobile the folders should be 2 x 2 still") — a single row of 4 squares reads as
   too cramped/tiny at phone widths. */
@media (max-width: 768px) {
  .category-folder-landing-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Wider (90%->96%/360px->420px), then 15% smaller again per a further direct follow-up
       (96%*0.85≈82%, 420px*0.85≈357px) — net a bit smaller than the original 90%/360px.
       82%->92%/357px->420px again per further direct follow-up ("less padding on the left and
       right of the screen for the cards") — widens the grid back out (less side margin) without
       touching the cards' own height/shape, which were separately tuned to fix the square/
       rectangle rendering bug above. */
    width: 92%;
    max-width: 420px;
    gap: 16px;
    /* -30px desktop, -5px mobile (smaller nudge — mobile isn't vertically centered inside a huge
       flex area the way desktop is, so it doesn't need as large an offset to read as "moved up"). */
    transform: translateY(-5px);
  }
  /* REAL BUG, found and fixed: .category-folder-card/-name/-count's own mobile overrides used to
     live here too, but this block sits BEFORE those selectors' base (unconditional) rules further
     down this file (.category-folder-card at ~310, -name/-count at ~345/~355) — same specificity,
     later source order, so the base rules were silently winning on every screen size, mobile
     included. That's why an earlier "10% smaller text" request never visibly took effect, and
     (more consequentially) why aspect-ratio wasn't actually reaching 1:1 on mobile either: at the
     full un-shrunk 18px/16px text, a longer folder name ("Publications"/"Board Games") wrapped
     onto 2 lines within the narrowed mobile card width, and a box's own automatic minimum height
     (based on its content) can still force it taller than aspect-ratio's preferred square size
     when content doesn't fit — aspect-ratio isn't a hard clip. The real overrides now live in
     their own block right after those base rules, where source order actually favors them. */
}
/* Solid purple fill with white icon/text, per direct follow-up — was purple-outlined/transparent
   (deliberately distinct from Music's own solid-fill cards at the time); overridden here since
   this is now the more recent direct instruction. */
.category-folder-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  aspect-ratio: 1 / 1;
  padding: 22px 10px;
  border: none;
  border-radius: 16px;
  background: var(--primary);
  cursor: pointer;
  text-align: center;
  transition: opacity 0.15s;
  /* Defensive, alongside the grid's own minmax(0, 1fr) fix above — a grid/flex item's default
     min-width is 'auto' (its content's own size), so this card could still refuse to shrink below
     its name text's width without this, even with a properly-shrinkable track. */
  min-width: 0;
}
.category-folder-card:hover {
  opacity: 0.85;
}
.category-folder-card-icon {
  display: flex;
  color: #fff;
}
/* folderIconHtml() (utils.js) bakes fill="#5B5BEF" onto the <svg> itself (inherited by its child
   <path>, which sets no fill of its own) — a real CSS fill declaration on the same element still
   overrides that presentation attribute (standard CSS-over-presentation-attribute precedence), so
   this doesn't need any change on the shared helper itself, just this one override. */
.category-folder-card-icon svg {
  fill: #fff;
}
.category-folder-card-name {
  /* 15px -> 18px (20% larger), per direct follow-up. */
  font-size: 18px;
  font-weight: 600;
  color: #fff;
  /* Wraps onto a second line rather than forcing the card wider on a narrow phone (a longer name
     like "Publications"/"Board Games" at 18px was part of what could blow the grid out — see the
     minmax(0, 1fr)/min-width: 0 fixes above). */
  overflow-wrap: break-word;
}
.category-folder-card-count {
  /* 13px -> 16px (20% larger), per direct follow-up. */
  font-size: 16px;
  color: #fff;
  opacity: 0.8;
}
/* Real mobile overrides for the three rules above — placed here, AFTER them, so source order
   actually favors these (see the REAL BUG comment near .category-folder-landing-grid's own mobile
   block, above, for why an earlier attempt positioned before these didn't work). Square (1:1) per
   direct follow-up ("make sure the cards sizes are square") — text is 15% smaller than the base
   18px/16px (15px/14px) specifically so a wrapped 2-line folder name still fits within a square
   card's own preferred height instead of forcing it taller. */
@media (max-width: 768px) {
  /* A fixed, explicit height — not aspect-ratio — per direct follow-up ("they still look like
     rectangles"/"they need to be less tall") after align-items: start + grid-auto-rows:
     min-content (above) didn't fully resolve it live on an iPhone 16 Pro. An explicit height
     gives the card a definite block-size outright, sidestepping aspect-ratio's dependency on the
     grid's own (still seemingly inconsistent in practice here) row-sizing behavior entirely,
     rather than continuing to chase exactly which part of that interaction WebKit was getting
     wrong. Not literally square anymore on mobile as a result — a deliberate trade for something
     that reliably renders short regardless of grid quirks. Explicitly resets the base rule's
     aspect-ratio: 1/1 back to none here too, rather than just leaving height to quietly take
     priority over it — belt and suspenders against any residual influence. */
  .category-folder-card {
    aspect-ratio: none;
    /* 10% smaller again (130px -> 117px), per direct follow-up ("make the folder cards 10%
       smaller"). Grid width (above) untouched — that governs each card's actual WIDTH and was
       just widened for less side padding, per a separate direct follow-up; shrinking it back down
       here would fight that. This height is the one property that's independently "the card's
       own size" outside of what the grid track dictates. */
    height: 117px;
  }
  .category-folder-card-name {
    /* 15px -> 14px, per direct follow-up ("decrease the font also"). */
    font-size: 14px;
  }
  .category-folder-card-count {
    /* 14px -> 13px, same follow-up. */
    font-size: 13px;
  }
  /* Icon size explicitly left alone, per direct follow-up ("leave the icons their current
     size") — folderIconHtml() (utils.js) bakes a fixed px width/height straight onto the <svg>
     itself (not CSS/em-relative), so it was already unaffected by the card/font shrink above;
     noted here so a future change doesn't assume it needs its own override too. */
}

/* Wraps the folder-card grid above + the demo carousel below (categoryCarousel.js) — plain
   #cards-grid used to just BE the grid directly (container.className was the grid's own class);
   now it's a column layout holding both pieces, so container.className moved up one level. */
/* Fills the available height and centers its own content vertically instead of stacking with a
   large fixed top margin — per direct follow-up ("these landing pages should not be scrolling up
   and down"). Paired with .grid-area:has(.category-landing-page) below, same established pattern
   as .grid-area:has(.kanban-wrap) (kanban.css): the page fills exactly the available space below
   the header and never scrolls itself, unlike every other view (which still needs normal
   overflow-y:auto for long lists). Desktop only now — per a further direct follow-up ("the page
   can scroll in mobile... just not desktop"), mobile gets normal scrolling back below, since
   there isn't enough height there to actually fit the shorter (see the mobile aspect-ratio
   override above) folder cards + carousel without it. */
.category-landing-page {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* NOT align-items: center — .category-carousel-wrap needs to stay stretched to the full page
     width for its own centering math to hold (it assumes it spans close to the whole content
     area); .category-folder-landing-grid centers itself instead, via its own explicit width +
     align-self: center (below), same as before. */
  gap: 24px;
  flex: 1;
  min-height: 0;
}
.grid-area:has(.category-landing-page) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
@media (max-width: 768px) {
  .grid-area:has(.category-landing-page) {
    overflow-y: auto;
    overflow-x: hidden;
  }
  .category-landing-page {
    justify-content: flex-start;
    /* REAL BUG, found and fixed: this element's own flex:1; min-height:0; (base rule above) is
       what let .grid-area's desktop overflow:hidden fixed-fit layout compress it to exactly fit
       the available space with no scrolling — but that same shrink-to-fit behavior was still in
       effect here on mobile too, silently defeating the overflow-y:auto override just above and
       this page's own --fab-clearance bottom padding (misc.css's base .grid-area rule): with
       min-height:0, this element obediently shrinks to fill whatever box .grid-area gives it
       instead of growing to its true (taller) content size, so .grid-area never actually had
       anything to scroll into — the folder cards + carousel just got squeezed into a shorter box,
       and the FAB stayed visually overlapping the carousel regardless of how much clearance
       padding was added (reported live: "the add item button is still overlapping the carousel in
       mobile"). flex-shrink:0 + min-height:auto let it size to its own natural, un-squeezed
       content height instead, so it can genuinely exceed the viewport and make .grid-area scroll
       for real. */
    flex-shrink: 0;
    min-height: auto;
  }
}

/* ===== Category landing carousel (categoryCarousel.js) — a center-emphasis horizontal scroller,
   demo content for now, added below the folder cards on every category landing page except
   Musician/Music Album, per direct request. The centered slide (JS-tracked via scroll position,
   not a real carousel library) scales up; side slides stay small. No on-image title/description
   text on any slide (removed per direct follow-up, "remove the text on the carosel cards") — the
   active slide's title still surfaces via .category-carousel-caption below the whole strip. ===== */
/* "Featured Saves" title (was "Recent Saves", renamed per direct follow-up) + divider above the
   carousel strip, per direct request. Own wrapper (not
   two separate flex children of .category-landing-page) so the tight title-to-divider spacing
   here is independent of that parent's own 24px gap between its top-level sections. */
.category-carousel-heading-wrap {
  width: 100%;
}
.category-carousel-heading {
  font-size: 18px;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 10px;
  text-align: center;
}
/* Matches the folder cards' own mobile name text size exactly (.category-folder-card-name, 14px
   — below), per direct follow-up ("make the recent saves text the same size as the card font"),
   rather than a separately-tuned value that could drift out of sync if the card text ever
   changes again. */
@media (max-width: 768px) {
  .category-carousel-heading {
    font-size: 14px;
  }
}
.category-carousel-divider {
  height: 1px;
  background: var(--border);
  width: 100%;
  /* Per direct request ("drop down the divider line 8px"). */
  margin-top: 8px;
}
.category-carousel-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}
/* Per direct follow-ups ("bring the carousel up 15px" — desktop, then another 10px: -25px). */
@media (min-width: 769px) {
  .category-carousel-wrap {
    margin-top: -25px;
  }
}
/* "Bring the carousel up 20px", per direct follow-up, confirmed to apply to both desktop and
   mobile — desktop's earlier +20px margin-top (from a prior "bring it down 20px" request) is
   removed outright (net back to its original position, base rule above has no margin-top at all),
   mobile gets a new -20px nudge since it had none before. */
@media (max-width: 768px) {
  .category-carousel-wrap {
    /* -20px -> -30px -> -35px -> -38px, per further direct follow-ups. */
    margin-top: -38px;
    /* Full-bleed to the actual screen edges, per direct follow-up ("make the carosol container
       touch the edges of the screen") — negative margins cancel out .grid-area's own mobile side
       padding (--page-mobile-pad, misc.css) exactly, rather than a hand-derived literal that'd
       drift if that padding ever changes. width:auto lets the negative margins do the widening
       instead of fighting the base rule's width:100% (which is 100% of the *padded* content box,
       not the screen). */
    margin-left: calc(-1 * var(--page-mobile-pad));
    margin-right: calc(-1 * var(--page-mobile-pad));
    width: auto;
  }
}
/* The active/center slide's own title, below the whole carousel — per direct request ("put the
   title center cards title below it in purple text centered below all the cards"). Kept alongside
   the existing on-image overlay title, not a replacement for it. Updated in JS whenever the
   active slide changes (_updateActiveSlide, categoryCarousel.js). */
.category-carousel-caption {
  /* Negative — counteracts the strip's own 30px bottom padding (cards.css, headroom for the
     active slide's scale-up) plus .category-landing-page's 24px flex gap between this and the
     carousel above, netting out to sit ~15px below the active card's actual bottom edge, per
     direct follow-up ("bring the center carosel card title up so it's 15px below the center
     card"). */
  margin-top: -39px;
  text-align: center;
  font-size: 18px;
  font-weight: 700;
  color: var(--primary);
  /* A real <button> now, per direct request ("make it so on desktop and mobile the center card
     title purple text is a link to that card") — reset to look exactly like the plain text it
     was, since the browser's own button chrome would otherwise add a border/background/system
     font. width: 100% since a button defaults to shrink-to-fit (inline-block-like) rather than
     the block-level <div> this used to be, which is what let text-align: center work at all. */
  display: block;
  width: 100%;
  border: none;
  background: none;
  padding: 0;
  font-family: inherit;
  cursor: pointer;
}
.category-carousel-caption:hover {
  text-decoration: underline;
}
/* The prev/next buttons also carry .dash-carousel-prev/.dash-carousel-next (categoryCarousel.js)
   so _wireCarouselArrows (dashboard.js) can find and wire them for its infinite-loop mechanics —
   dashboard.css's own same-specificity .dash-carousel-prev/.dash-carousel-next rule loads AFTER
   this file, so this compound selector (two classes, not one) is needed to keep this look winning
   regardless of load order. */
.category-carousel-arrow.dash-carousel-prev,
.category-carousel-arrow.dash-carousel-next {
  flex-shrink: 0;
  border: none;
  background: none;
  color: var(--text-secondary);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28px;
  line-height: 1;
  padding: 4px 6px;
  transition: color 0.15s;
}
.category-carousel-arrow.dash-carousel-prev:hover,
.category-carousel-arrow.dash-carousel-next:hover {
  color: var(--primary);
}
/* Hidden on mobile, per direct follow-up ("remove the arrows") — swiping the strip directly
   already works (overflow-x: auto below), the arrows were just extra chrome eating into the
   now-edge-to-edge width above. Same compound-selector specificity as the base rule right above
   (a plain .category-carousel-arrow { display: none; } here would lose to it) — placed after in
   source order so this wins on mobile. */
@media (max-width: 768px) {
  .category-carousel-arrow.dash-carousel-prev,
  .category-carousel-arrow.dash-carousel-next {
    display: none;
  }
}
.category-carousel-strip {
  flex: 1;
  display: flex;
  align-items: center;
  /* REAL BUG, found and fixed: on a wide screen, 7 demo slides can be narrower than the strip's
     own viewport, so there's nothing to scroll — flex's default justify-content: flex-start then
     left-aligns the whole row flush against the left arrow instead of centering it, so the
     "active" card landed left of the page's true center (reported live, screenshot confirmed).
     justify-content: center fixes the no-overflow case; once there IS enough content to actually
     scroll, this has no visible effect and the JS-driven scrollLeft centering (categoryCarousel.js)
     takes over instead. */
  justify-content: center;
  gap: 18px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* Generous vertical padding — the active slide scales up past its own box (transform: scale),
     so the strip needs headroom above/below it or the enlarged slide would visibly clip against
     the strip's own edges. */
  padding: 30px 8px;
  /* REAL BUG, found and fixed: fixed here at the active slide's own max height (240px) + padding
     (30px top/bottom) now that .category-carousel-slide's width/height actually animate (the
     zoom-out fix above) — before that fix this was a non-issue (the box size snapped instantly,
     so there was nothing to visibly reflow around), but with the size now easing over 0.3s, the
     strip's own height (previously undeclared, driven purely by whichever slide inside it was
     currently tallest) was recalculating on every animation frame too, pushing the caption below
     the carousel — and everything under IT on the page — up and down for the whole transition.
     Reserving the full height up front means an individual slide growing/shrinking now happens
     entirely within a stable envelope instead of resizing the row itself. */
  height: 300px;
  scrollbar-width: none;
  /* Click-and-drag panning (dashboard.js's _wireCarouselArrows) — grab/grabbing affordance so a
     mouse user can tell this is draggable, same as the arrows already imply prev/next is. */
  cursor: grab;
}
.category-carousel-strip::-webkit-scrollbar { display: none; }
/* .category-carousel-strip.carousel-dragging's own cursor/user-select rule now lives in
   dashboard.css, merged with .dash-carousel-strip's identical copy (/simplify pass) — both
   carousels use the same _wireCarouselArrows drag mechanic. */
/* Toggled briefly during initCategoryCarousel()'s first-paint centering measurement
   (categoryCarousel.js) — see its own comment for why: forces the active slide to its final
   settled size/position INSTANTLY for that one measurement, instead of the measurement catching
   it still mid-zoom-transition and undershooting the centering math. */
.category-carousel-strip--no-transition .category-carousel-slide {
  transition: none !important;
}
/* Same toggle window, targeting the strip itself rather than its slides — REAL BUG, found and
   fixed: iOS Safari (where every mobile centering report so far has come from) can apply
   scroll-snap-type: x mandatory's own snap settling to a programmatic scrollLeft assignment, not
   just real user gestures, sometimes asynchronously — so the precise centered value
   initCategoryCarousel() computes could still get nudged to whichever slide's own
   scroll-snap-align: center point the browser considered "nearest" afterward, landing slightly
   off from the intended target specifically on mobile (desktop wasn't affected, hence this
   surviving the earlier scroll-behavior: smooth fix — reported live: "in mobile the center card
   is not centered", confirmed via follow-up that scrolling manually *does* land correctly,
   isolating this to the one-time initial placement specifically). Suppressed for the same
   instant-snap window as the transition rule above, so nothing can second-guess the computed
   position before it's actually painted; normal scroll-snap resumes immediately after for real
   user interaction. */
.category-carousel-strip--no-transition {
  scroll-snap-type: none !important;
}
/* .category-carousel-strip *'s own user-drag/user-select rule also now lives in dashboard.css,
   merged with .dash-carousel-strip *'s identical copy — see that file's comment for why (native
   image/text drag hijacking the click-and-drag panning gesture). */
/* Desktop-only edge fade, per direct request ("similar to the dashboard fade") — same technique
   and percentages as .dash-hero-collage's own mask (dashboard.css): a mask-image gradient stays
   pinned to the strip's own visible viewport as its content scrolls beneath it (mask-attachment's
   default 'scroll' value, same as background-attachment — NOT 'local', which would scroll along
   with the content instead), so the slides nearest each edge fade out smoothly rather than being
   abruptly clipped by the strip's own overflow-x. Desktop only per the request; mobile's own
   edge-to-edge carousel (above) was deliberately made full-bleed to the screen edges, so fading
   there would fight that. */
@media (min-width: 769px) {
  .category-carousel-strip {
    mask-image: linear-gradient(to right, transparent, black 15%, black 85%, transparent);
    -webkit-mask-image: linear-gradient(to right, transparent, black 15%, black 85%, transparent);
    /* 18px -> 9px (half, per direct request) -> 19px (+10px, per direct follow-up — "the gutter
       is now too narrow increase the gutter by 10px", once the scale-compensation margins below
       made the change actually visible) — only the active slide overlaps its neighbors (its own
       negative margin, above); every other slide-to-slide spacing is this gap. */
    gap: 19px;
  }
}
/* A <button> now (categoryCarousel.js — clicking opens the item's real detail modal, genuine
   behavior now that these are real save/curated items, not decorative placeholder art), so the
   browser's own button chrome needs resetting; the background image is a real <img> child
   (.category-carousel-slide-img) rather than an inline background-image style, since these are
   now real, possibly-missing item.imageUrl values (some curated/admin-configured cards have none)
   that need the same onerror-hide fallback pattern the Dashboard's own thumbnails use. */
.category-carousel-slide {
  position: relative;
  flex-shrink: 0;
  width: 170px;
  height: 170px;
  border-radius: 16px;
  overflow: hidden;
  scroll-snap-align: center;
  cursor: pointer;
  /* 0.92 -> 0.65, per direct follow-up ("have them all completely zoom out... zoom and fade
     completely to black as they zoom fade into the next slide") — a much more dramatic shrink so
     the active slide reads as the overwhelming visual focus and everything else genuinely
     "zooms out" rather than just sitting slightly smaller. */
  transform: scale(0.65);
  /* 0.75 -> 0.55 -> 0.12, per the same follow-up — against this app's dark theme, fading opacity
     this far down reads as "fading to black" without needing an actual black overlay layer. */
  opacity: 0.12;
  /* REAL BUG, found and fixed: width/height/margin (the slide's actual box size — 170px resting
     vs. 240px active, .category-carousel-slide--active below) weren't in this list, only
     transform/opacity/box-shadow/border-color were — so when the active slide loses that class
     as a new one takes over, the box itself SNAPPED instantly to its smaller footprint while only
     the scale factor eased on top of that already-shrunk box, reading as an abrupt resize with a
     faint secondary wobble rather than one continuous zoom out. Per direct request ("make the
     center image zoom out as it gets replaced") — width/height/margin now animate too, so the
     whole shrink (box size + scale + the overlap margin easing back to 0) reads as a single
     smooth zoom rather than a snap-then-adjust. */
  /* 0.3s ease -> 0.6s ease-in-out (too slow, per direct follow-up) -> 0.3s ease-in-out — sped
     back up per direct follow-up ("the zooms are too slow speed them up"), keeping the gentler
     ease-in-out curve from that slow-down pass since only the speed was the complaint. */
  transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out, box-shadow 0.3s ease-in-out,
    border-color 0.3s ease-in-out, width 0.3s ease-in-out, height 0.3s ease-in-out, margin 0.3s ease-in-out;
  border: 3px solid transparent;
  padding: 0;
  background: var(--hover-bg);
  font: inherit;
  text-align: left;
}
.category-carousel-slide-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Same gradient-plus-icon fallback every plain item card already uses when it has no image
   (.card-placeholder/.placeholder-<Category>, above) — here filling the whole slide the way
   .category-carousel-slide-img does, with a bigger icon to suit the larger box. */
.category-carousel-slide-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Large white letter, per direct follow-up ("change the icon to a white first letter of the
     item. make that letter large") — same bold weight .card-placeholder's own monogram uses,
     just bigger to suit this larger box. */
  font-size: 72px;
  font-weight: 700;
  color: #fff;
  letter-spacing: -0.02em;
}
.category-carousel-slide-demo-badge {
  position: absolute;
  /* Lower right corner, per direct request — was top-left. */
  bottom: 8px;
  right: 8px;
  background: var(--primary);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  border-radius: 10px;
  padding: 2px 7px;
  z-index: 1;
}
/* Real (non-demo) slide's folder-name tag — same lower-right spot the Demo badge occupies on a
   demo slide (they're mutually exclusive per slide, see categoryCarousel.js), per direct request.
   Transparent black pill, gray lettering, no icon — deliberately understated next to the Demo
   badge's solid purple fill. */
.category-carousel-slide-folder-badge {
  position: absolute;
  bottom: 8px;
  right: 8px;
  background: rgba(0, 0, 0, 0.55);
  color: #D1D5DB;
  font-size: 10px;
  font-weight: 700;
  border-radius: 10px;
  padding: 2px 7px;
  z-index: 1;
}
/* The active/center slide's own folder tag stays purple (same treatment the Demo badge already
   gets on the active slide) rather than the gray/transparent-black every other slide uses, per
   direct follow-up ("the center item card... should still have the purple colored tag"). */
.category-carousel-slide--active .category-carousel-slide-folder-badge {
  background: var(--primary);
  color: #fff;
}
/* Light gray instead of purple on every slide except the active/center one, per direct follow-up
   — keeps the purple reserved for the one card that's actually the visual focus. */
.category-carousel-slide:not(.category-carousel-slide--active) .category-carousel-slide-demo-badge {
  background: #D1D5DB;
  color: #1F2328;
}
/* The ✨ icon drops too on every non-active badge, per direct follow-up — just the plain "Demo"
   text there. */
.category-carousel-slide:not(.category-carousel-slide--active) .category-carousel-slide-demo-badge-icon {
  display: none;
}
.category-carousel-slide--active {
  width: 240px;
  height: 240px;
  transform: scale(1);
  opacity: 1;
  /* Purple, not white, per direct follow-up. */
  border-color: var(--primary);
  box-shadow: 0 12px 32px rgba(0,0,0,0.35);
  z-index: 2;
  /* Pulls the two neighbor slides in underneath it (negative margin eats into the strip's own
     18px gap on both sides) — per direct follow-up ("make the center card overlap the two behind
     it some"). z-index above wins the overlap, same as the reference image's own layered look. */
  /* -26px -> -50px (more overlap), per direct follow-up. */
  margin: 0 -50px;
}
/* The two slides immediately beside the active/center one get their own intermediate opacity/
   scale step, per direct request ("as the scroll happens make the two cards on the left and
   right of the center card slightly fade down and slightly zoom down") — between the active
   slide's full opacity: 1/scale(1) and every farther-out slide's dimmer opacity/scale (base rule,
   above). :has()/adjacent-sibling (no JS) so this tracks whichever slide is actually
   active automatically as _updateActiveSlide (categoryCarousel.js) toggles that class during
   scroll — same live-relationship idiom as the mobile-only sizing tier below, just for
   opacity/scale and not restricted to mobile. Already animates smoothly (not a snap) since
   opacity/transform are both already in .category-carousel-slide's own transition list. */
.category-carousel-slide--active + .category-carousel-slide,
.category-carousel-slide:has(+ .category-carousel-slide--active) {
  /* 0.8 -> 0.35, 0.97 -> 0.85 — pushed much further down, per direct follow-up ("have them all
     completely zoom out... zoom and fade completely to black"), same reasoning as the base
     rule's own steeper values just above. */
  opacity: 0.35;
  transform: scale(0.85);
}
/* The next pair out — the edge-touching slides, per direct follow-up ("then the cards to the
   left and right of those cards slightly fade down and slightly zoom down... these should zoom
   smaller than those next to them... these are the cards touching the screen's edge") — a third,
   even more faded/scaled-down step beyond the immediate-neighbor tier above. Chains the same
   :has()/adjacent-sibling technique one slide further out (":has(+ X + Y)" reads as "followed by
   an X, followed by a Y" — the relative-selector combinator chain is what lets this skip past the
   immediate neighbor to target the one after it). These numbers happen to match
   .category-carousel-slide's own base rule already (nothing else needs this dim/small by
   default) — written explicitly here anyway so the full 3-tier cascade reads as one deliberate
   design, not an incidental fallback. */
.category-carousel-slide--active + .category-carousel-slide + .category-carousel-slide,
.category-carousel-slide:has(+ .category-carousel-slide + .category-carousel-slide--active) {
  /* 0.12 -> 0.35 (matches the neighbor/2nd tier's own opacity above), per direct request ("make
     the 3rd tiers the level of dimness the 2nd currently is. the 3rd looks too dim") —
     deliberately no longer matching the base/4th+ tier's own 0.12 (this used to read as "the
     same as the catch-all," per the comment above; that's no longer true for opacity, just the
     scale/margin still shared with it via cards.css's desktop split further down). */
  opacity: 0.35;
  transform: scale(0.65);
}
/* Desktop-only tier sizing — active/2nd(neighbor)/3rd/4th+, per many rounds of direct live
   tuning. /simplify pass: this used to be TWO separate @media (min-width: 769px) blocks (one
   for margin, one for transform), split purely because of a real cascade-ordering bug (a
   same-specificity rule positioned before the general tier rules above loses to them regardless
   of media-query nesting — reported live as "i don't see the change" when a scale edit landed in
   the wrong block and got silently overridden back), with each tier's margin hand-computed via
   "170 * (1 - scale) / 2" and kept in sync by hand every time its scale changed (this drifted out
   of sync at least twice, also reported live: "the gutter change was only applied to the second
   card"). Consolidated into one block, positioned after the general rules so it reliably wins,
   driving both transform and margin from a single --tier-scale custom property per tier: only
   the base (least-specific) selector needs the actual transform/margin formula, since CSS
   applies each property from whichever rule declares it, independent of which rule "wins" as a
   whole — a tier selector that sets only --tier-scale still gets transform/margin from the base
   rule below, just with ITS OWN --tier-scale substituted in. Changing a tier's size is now a
   one-line edit instead of two hand-paired numbers. */
@media (min-width: 769px) {
  .category-carousel-slide:not(.category-carousel-slide--active) {
    --tier-scale: 0.68; /* 4th position and beyond (the catch-all) */
    transform: scale(var(--tier-scale));
    /* 170px slide width / 2 = 85px half-width. Derives the compensating margin automatically
       from whatever --tier-scale a given tier resolves to — positive when --tier-scale > 1 (the
       painted card overflows its own layout box, so neighbors need to be pushed away), negative
       when < 1 (the painted card shrinks inside its box, so neighbors need to be pulled closer)
       — same formula either way, no separate hand-derived cases. */
    margin: 0 calc((1 - var(--tier-scale)) * -85px);
  }
  .category-carousel-slide--active + .category-carousel-slide + .category-carousel-slide,
  .category-carousel-slide:has(+ .category-carousel-slide + .category-carousel-slide--active) {
    --tier-scale: 0.85; /* 3rd position */
  }
  .category-carousel-slide--active + .category-carousel-slide,
  .category-carousel-slide:has(+ .category-carousel-slide--active) {
    --tier-scale: 1.0; /* 2nd/neighbor position — the one overlapped by the active slide */
  }
}
/* Smaller on mobile, per direct follow-up ("reduce the large images size in the carousel in
   mobile") — the desktop sizes (170px resting / 240px active) left little headroom on a phone.
   Placed after the base rules above (not inside the earlier mobile block near the folder cards)
   so this — same specificity, later in source order — actually wins over them.
   120px/170px -> 15% smaller again (102px/145px, later tuned to 130px for the active tier — see
   its own more specific comment below), per further direct follow-up ("make the cards
   15% smaller in mobile"), arriving mid-carousel-heading-work so read as this carousel rather
   than the folder cards (already resized/reshaped repeatedly just prior). Active's overlap margin
   scaled down proportionally too (-30px -> -25px) so it still reads the same relative amount of
   overlap into the smaller neighbor slides. */
@media (max-width: 768px) {
  /* Now the smallest/outermost tier (was every non-active slide's size) — per direct follow-up
     ("so there are 3 card sizes"), this base size is only what's left once the two more specific
     tiers below (immediate neighbors, active) claim their own slides. */
  .category-carousel-slide {
    width: 80px;
    height: 80px;
  }
  /* The two slides immediately beside the active/center one — a middle tier between the small
     edge slides above and the active slide below, overlapping into the edge slides on their own
     outer side the same way the active slide overlaps into these, per direct follow-up ("the two
     cards behind the center card... overlap the cards on their sides touching the edges... a bit
     [bigger than] the edge cards"). Pure CSS (:has()/adjacent-sibling, no JS) so this tracks
     whichever slide is actually active automatically as _updateActiveSlide (categoryCarousel.js)
     toggles that class during scroll — same live-relationship idiom as this codebase's other
     :has() usages (e.g. .grid-area:has(<page>)), just keyed off a sibling instead of a descendant.
     Specificity (0,2,0, two classes) beats the base rule above regardless of source order. */
  .category-carousel-slide--active + .category-carousel-slide,
  .category-carousel-slide:has(+ .category-carousel-slide--active) {
    width: 102px;
    height: 102px;
    margin: 0 -18px;
    z-index: 1;
  }
  /* Active/center card, 10% smaller again (145px -> 130px) + more overlap into its neighbor
     slides (-25px -> -40px), per direct follow-up ("make the center card 10% smaller overlap the
     two cars behind it more"). */
  .category-carousel-slide--active {
    width: 130px;
    height: 130px;
    margin: 0 -40px;
  }
  /* Mobile-sized counterpart to the desktop fixed height above (300px = 240px active slide +
     30px top/bottom padding) — 130px active slide + the same 30px padding = 190px. Same "stop
     the strip resizing itself around the animating active slide" fix, just at mobile's own
     active-slide size. */
  .category-carousel-strip {
    height: 190px;
  }
}
/* One-shot entrance for whichever slide just became active, per direct request ("the large center
   feature item should change to the next item, come from the left side") — toggled on/off in JS
   (categoryCarousel.js) each time the active slide changes, rather than left permanently applied,
   so it replays every time instead of only once. Ends at .category-carousel-slide--active's own
   resting transform/opacity so there's no visual jump when the class is removed post-animation. */
@keyframes categoryCarouselEnterFromLeft {
  from { transform: translateX(-40px) scale(0.85); opacity: 0.5; }
  to { transform: translateX(0) scale(1); opacity: 1; }
}
/* 0.4s -> 0.7s (too slow, per direct follow-up) -> 0.3s — kept in step with the resize
   transition above (also sped back up to 0.3s) so the entrance and the outgoing zoom-out still
   read as one coordinated motion. */
.category-carousel-slide--entering {
  animation: categoryCarouselEnterFromLeft 0.3s ease-out;
}
.grid-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 20px;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 16px;
}

/* The "Top 100" word on a Top 100 drilldown's title (e.g. "Top 100 Books") — links back to the
   landing page (renderCuratedGenreLanding() in render.js). Reads as ordinary title text, not an
   obvious button, until hovered. */
.grid-title-link {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.grid-title-link:hover {
  color: var(--primary);
  text-decoration: underline;
}

/* "Sources | Civics" — the active Saved List's name, appended to the page title while browsing
   inside that list (renderGrid.js). Purple (not just on hover, unlike .grid-title-link above) so
   the active scope stays visually obvious at a glance; underlines on hover as a real link back to
   that list's own landing card. */
.grid-title-scope-link {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: var(--primary);
  cursor: pointer;
}

.grid-title-scope-link:hover {
  text-decoration: underline;
}

/* News outlet folder header — domain + paywalled badge next to the publication name */
.grid-title-domain {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
}

.grid-title-paywalled-badge {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  background: var(--hover-bg);
  border-radius: 10px;
  padding: 2px 8px;
}

/* .nyt-logo-wrap/.rs-logo-wrap/.steam-logo-wrap: rendered inside #grid-title itself, per request
   (.grid-title's own row — that's a flex row too, see its own rule above), not the divider-bar-
   after-text treatment it used to have inline right after the title text. margin-left: auto pushes
   it to the row's right edge, matching #sort-select's own equivalent in .grid-header just below. */
.nyt-logo-wrap {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}

.nyt-logo-wrap svg {
  height: 17px;
  width: auto;
  fill: rgba(255,255,255,0.7);
  display: block;
}

.rs-logo-wrap {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}

.rs-logo-img {
  height: 22px;
  width: auto;
  display: block;
  opacity: 0.7;
  filter: brightness(0) invert(1);
}

.steam-logo-wrap {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}

.steam-logo-img {
  height: 45px;
  width: auto;
  display: block;
  opacity: 0.7;
  filter: brightness(0) invert(1);
}

.nyt-logo-wrap,
.rs-logo-wrap,
.steam-logo-wrap {
  position: relative;
  cursor: default;
}

.nyt-logo-wrap::after,
.rs-logo-wrap::after,
.steam-logo-wrap::after {
  content: 'Top 100 list populated using data from this organization.';
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  width: 220px;
  padding: 10px 12px;
  background: var(--surface);
  color: var(--text-primary);
  font-size: 12px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  line-height: 1.4;
  border-radius: 8px;
  border: 1px solid var(--border);
  box-shadow: 0 6px 20px rgba(0,0,0,0.35);
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(-4px);
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
  pointer-events: none;
  z-index: 20;
  text-align: left;
}

.nyt-logo-wrap:hover::after,
.rs-logo-wrap:hover::after,
.steam-logo-wrap:hover::after {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}

/* ===== CARDS GRID ===== */
.cards-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  align-items: start;
}

/* ===== CARD ===== */
.card {
  background: var(--surface);
  border-radius: 12px;
  border: 1px solid var(--border);
  overflow: hidden;
  cursor: pointer;
  transition: box-shadow 0.2s, transform 0.1s;
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  height: 110px;
}

.card:hover {
  box-shadow: 0 4px 20px var(--shadow-card);
  transform: translateY(-1px);
}

/* Owns the 100px slot + local clip boundary (REAL BUG, found and fixed: a scaled .card-image--zoom
   painted past its own layout box and overlapped .card-body next to it — see that class's own
   comment — since .card's own overflow: hidden only clips against the whole card, not this
   specific slot). .card-image/.card-placeholder now just fill 100% of this wrapper instead of
   sizing themselves directly. */
.card-image-crop {
  width: 100px;
  height: 100%;
  flex-shrink: 0;
  overflow: hidden;
  position: relative;
}

/* Left thumbnail — white background behind the image itself (not just the card), since some
   curated logos (TV show/movie logo PNGs especially) have transparent backgrounds and were
   nearly invisible against the dark card background showing through. */
.card-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
  background: #fff;
}
/* Extra zoom-crop for YouTube thumbnails specifically (isYoutubeThumbnailUrl, utils.js) — some
   have real black letterboxing/pillarboxing baked into the image's own pixels (fetchVideoThumbnail's
   own comment, api.js) that object-fit: cover alone can shrink but not fully crop away on whichever
   axis isn't the one already being cropped, per direct report ("i don't want to see black bands...
   full bleed the image container"). .card-image-crop's own overflow: hidden (above) clips the
   excess locally now, instead of relying on .card's own overflow: hidden further out.  */
.card-image--zoom {
  /* 1.2 -> 1.25 -> 1.35, per direct follow-ups. */
  transform: scale(1.35);
}

.card-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 36px;
  font-weight: 700;
  color: rgba(255,255,255,0.9);
  letter-spacing: -0.02em;
}

/* Placeholder gradients */
.placeholder-Music-Album { background: linear-gradient(135deg, #EC4899 0%, #F9A8D4 100%); }
.placeholder-Musician    { background: linear-gradient(135deg, #EC4899 0%, #F9A8D4 100%); }
.placeholder-Show        { background: linear-gradient(135deg, #8B5CF6 0%, #C4B5FD 100%); }
.placeholder-Book        { background: linear-gradient(135deg, #10B981 0%, #6EE7B7 100%); }
.placeholder-Book-Author { background: linear-gradient(135deg, #10B981 0%, #6EE7B7 100%); }
.placeholder-Movie       { background: linear-gradient(135deg, #3B82F6 0%, #93C5FD 100%); }
.placeholder-Movie-Director { background: linear-gradient(135deg, #3B82F6 0%, #93C5FD 100%); }
.placeholder-Show-Creator   { background: linear-gradient(135deg, #8B5CF6 0%, #C4B5FD 100%); }
.placeholder-Game        { background: linear-gradient(135deg, #F59E0B 0%, #FCD34D 100%); }
.placeholder-Game-Studio { background: linear-gradient(135deg, #F59E0B 0%, #FCD34D 100%); }
.placeholder-Visual-Art  { background: linear-gradient(135deg, #F97316 0%, #FDBA74 100%); }
.placeholder-Memes       { background: linear-gradient(135deg, #EF4444 0%, #FCA5A5 100%); }
/* Web Links ("Sources") — REAL BUG, found and fixed: every other category had its own placeholder
   gradient + badge color, but Web Links never got one, so a card with no fetched/manual image
   (e.g. climate.us, reported live) just showed a blank, uncolored box instead of a colored
   fallback like every other category's cards do. Teal, to stay visually distinct from every color
   already in use above. */
.placeholder-Web-Links   { background: linear-gradient(135deg, #06B6D4 0%, #67E8F9 100%); }
/* Legacy names kept for any un-migrated items */
.placeholder-Music  { background: linear-gradient(135deg, #EC4899 0%, #F9A8D4 100%); }
.placeholder-Shows  { background: linear-gradient(135deg, #8B5CF6 0%, #C4B5FD 100%); }
.placeholder-Books  { background: linear-gradient(135deg, #10B981 0%, #6EE7B7 100%); }
.placeholder-Movies { background: linear-gradient(135deg, #3B82F6 0%, #93C5FD 100%); }
.placeholder-Games  { background: linear-gradient(135deg, #F59E0B 0%, #FCD34D 100%); }

/* Right content */
.card-body {
  flex: 1;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: 3px;
  min-width: 0;
  overflow: hidden;
}
/* Wraps the author/title/album-year text (renderCard(), renderGrid.js) separately from
   .card-meta's badge row — per direct request ("for all the categories can you center the card
   text in the cards"). flex: 1 claims all the vertical space .card-meta's own margin-top: auto
   isn't using, and justify-content: center vertically centers the text within THAT space, while
   .card-meta still ends up pinned to the very bottom of the card either way (it's the only
   sibling left after this wrapper's flex: 1 growth eats the rest) — same visual anchor as
   before, just with the text no longer stuck flush against the top above it. */
.card-text-block {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 3px;
  min-width: 0;
}

.card-title {
  font-size: 20px;
  font-weight: 600;
  color: var(--text-primary);
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.3;
}

.card-title--album {
  font-size: 16px;
}

.card-album-year {
  font-size: 10px;
  color: var(--text-muted);
  margin-top: 2px;
}

.card-author {
  font-size: 20px;
  font-weight: 600;
  color: var(--text-primary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.3;
}

.card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: auto;
}

.card-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 12px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.card-badge svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

/* Badge colors */
.badge-Music-Album  { background: #FCE7F3; color: #BE185D; }
.badge-Musician     { background: #FCE7F3; color: #BE185D; }
.badge-Show         { background: #EDE9FE; color: #6D28D9; }
.badge-Book         { background: #D1FAE5; color: #065F46; }
.badge-Book-Author  { background: #D1FAE5; color: #065F46; }
.badge-Movie        { background: #DBEAFE; color: #1E40AF; }
.badge-Movie-Director { background: #DBEAFE; color: #1E40AF; }
.badge-Show-Creator   { background: #EDE9FE; color: #6D28D9; }
.badge-Game         { background: #FEF3C7; color: #92400E; }
.badge-Game-Studio  { background: #FEF3C7; color: #92400E; }
.badge-Visual-Art   { background: #FFEDD5; color: #9A3412; }
.badge-Memes        { background: #FEE2E2; color: #991B1B; }
.badge-Web-Links    { background: #CFFAFE; color: #155E75; }

/* Genre-tag badge (renderGrid.js's renderCard()) — now REPLACES the Musician/Music Album category
   badge on these cards rather than sitting beside it (see renderCard()'s own comment), so per
   direct follow-up it's styled with the exact same pink fill .badge-Musician/.badge-Music-Album
   use above instead of the outlined/neutral look this used to have when it was secondary metadata
   next to a real category badge. Kept as its own class (not just reusing .badge-Musician directly)
   since its content is a free-form genre string, not a fixed category label. */
.card-badge-genre {
  background: #FCE7F3;
  color: #BE185D;
}
[data-theme="dark"] .card-badge-genre {
  background: #4A1830;
  color: #F9A8D4;
}

/* Legacy names kept for any un-migrated items */
.badge-Music  { background: #FCE7F3; color: #BE185D; }
.badge-Shows  { background: #EDE9FE; color: #6D28D9; }
.badge-Books  { background: #D1FAE5; color: #065F46; }
.badge-Movies { background: #DBEAFE; color: #1E40AF; }
.badge-Games  { background: #FEF3C7; color: #92400E; }
.badge-Memes  { background: #FEE2E2; color: #991B1B; }

[data-theme="dark"] .badge-Music-Album  { background: #4A1830; color: #F9A8D4; }
[data-theme="dark"] .badge-Musician     { background: #4A1830; color: #F9A8D4; }
[data-theme="dark"] .badge-Show         { background: #2E1A5E; color: #C4B5FD; }
[data-theme="dark"] .badge-Book         { background: #064E3B; color: #6EE7B7; }
[data-theme="dark"] .badge-Movie        { background: #1E3A5F; color: #93C5FD; }
[data-theme="dark"] .badge-Movie-Director { background: #1E3A5F; color: #93C5FD; }
[data-theme="dark"] .badge-Show-Creator   { background: #2E1A5E; color: #C4B5FD; }
[data-theme="dark"] .badge-Game         { background: #4A2C00; color: #FCD34D; }
[data-theme="dark"] .badge-Game-Studio  { background: #4A2C00; color: #FCD34D; }
[data-theme="dark"] .badge-Visual-Art   { background: #4A1A00; color: #FDBA74; }
[data-theme="dark"] .badge-Memes        { background: #4A1010; color: #FCA5A5; }
[data-theme="dark"] .badge-Web-Links    { background: #0E4B54; color: #67E8F9; }
/* Legacy dark names */
[data-theme="dark"] .badge-Music  { background: #4A1830; color: #F9A8D4; }
[data-theme="dark"] .badge-Shows  { background: #2E1A5E; color: #C4B5FD; }
[data-theme="dark"] .badge-Books  { background: #064E3B; color: #6EE7B7; }
[data-theme="dark"] .badge-Movies { background: #1E3A5F; color: #93C5FD; }
[data-theme="dark"] .badge-Games  { background: #4A2C00; color: #FCD34D; }
[data-theme="dark"] .badge-Memes  { background: #4A1010; color: #FCA5A5; }

/* Hover actions — right side of card */
.card-actions {
  display: none;
  align-items: center;
  gap: 4px;
  position: absolute;
  top: 8px;
  right: 8px;
}

.card:hover .card-actions {
  display: flex;
}

.card-action-btn {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: none;
  background: var(--hover-bg);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  color: #9AA0A6;
  transition: background 0.1s, color 0.1s;
}

.card-action-btn:hover {
  background: var(--border);
  color: var(--text-primary);
}

/* Quick "add to queue" bookmark on curated cards (see renderCard() in render.js) — same corner
   slot .card-actions uses for personal cards' edit/delete, but curated and personal cards never
   render both at once, so there's no overlap. Hidden until hover, same as .card-actions, EXCEPT
   once queued (--active) it stays permanently visible/purple as a status indicator instead of
   only showing on hover. */
.card-quick-queue-btn {
  display: none;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: none;
  background: var(--hover-bg);
  cursor: pointer;
  color: #9AA0A6;
  position: absolute;
  top: 8px;
  right: 8px;
  transition: background 0.1s, color 0.1s;
  z-index: 1;
}

.card:hover .card-quick-queue-btn,
.top100-row-card:hover .card-quick-queue-btn {
  display: flex;
}

.card-quick-queue-btn:hover {
  background: var(--border);
  color: var(--text-primary);
}

.card-quick-queue-btn--active {
  display: flex;
  background: var(--hover-bg);
  color: var(--primary);
}

.card-quick-queue-btn--active:hover {
  background: var(--border);
  color: var(--primary);
}

/* ===== EMPTY STATE ===== */
.cards-grid.landing-state {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - 160px);
  padding-bottom: 20vh;
}

.cards-grid.landing-state .empty-state {
  background: #5B5BEF;
  border-radius: 24px;
  padding: 48px 64px;
}

.cards-grid.landing-state .empty-state h3 {
  color: #fff;
}

.cards-grid.landing-state .empty-state p {
  color: rgba(255, 255, 255, 0.8);
}

.empty-state {
  grid-column: 1 / -1;
  grid-row: 1;
  text-align: center;
  padding: 80px 24px;
  color: var(--text-secondary);
}

.empty-state-icon {
  font-size: 56px;
  margin-bottom: 16px;
}

.empty-state h3 {
  font-size: 18px;
  font-weight: 500;
  margin-bottom: 8px;
  color: var(--text-primary);
}

.empty-state p {
  font-size: 14px;
  line-height: 1.6;
}

/* Share modal overrides */

/* ===== AUTHOR PAGES ===== */

.author-page-grid {
  display: block;
}

.author-page-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 20px;
}

.author-page-photo-wrap {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.author-page-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.author-page-photo-placeholder {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 38px;
  font-weight: 700;
  color: rgba(255,255,255,0.9);
  letter-spacing: -0.02em;
  flex-shrink: 0;
}

.author-page-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.author-page-name {
  font-size: 22px;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.2;
}

.author-page-name-btn {
  display: inline-flex;
  align-items: center;
  background: none;
  border: none;
  padding: 0;
  text-align: left;
  cursor: pointer;
  transition: color 0.15s;
}

.author-page-name-btn:hover {
  color: var(--primary);
}

.author-page-website {
  font-size: 12px;
  color: var(--primary);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.author-page-website:hover {
  text-decoration: underline;
}

/* Musician pages only (renderAuthorPage.js) — "Genre | url" sits where .author-page-website used
   to sit alone, per direct request. .author-page-genre-row wraps both pieces (align-self: flex-
   start needed — .author-page-info is a flex-direction: column container with the default
   align-items: stretch, which would otherwise stretch this to the column's full width). The
   genre pill itself is not tied to the per-category .card-badge/badge-Musician color map (that's
   keyed by SaveCraft category; there's no sane way to map iTunes' 50+ genre strings to it) — a
   plain neutral pill instead, same rounded-pill shape language as .card-badge. */
.author-page-genre-row {
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: 6px;
}
.author-page-genre {
  font-size: 11px;
  font-weight: 600;
  padding: 2px 10px;
  border-radius: 12px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  background: var(--hover-bg);
  color: var(--text-secondary);
}
.author-page-genre-sep {
  color: var(--text-muted);
  font-size: 12px;
}

/* Mobile-only duplicate of the website link, positioned below the header's own divider — hidden
   by default (desktop shows .author-page-website--inline, up in the genre row, instead); see the
   mobile media query below for the swap. */
.author-page-website-row {
  display: none;
  margin-bottom: 12px;
}

.author-works-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 12px;
}

/* .author-works-header (its parent, above) is uppercase — this button sits inside it, so it needs
   its own text-transform: none to keep "Fetch Albums" normal-case instead of inheriting that. */
#btn-fetch-albums {
  text-transform: none;
}

.author-works-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}

.author-no-works {
  color: var(--text-muted);
  font-size: 13px;
  grid-column: 1 / -1;
}

/* Non-clickable author name on cards (Music Albums section) */
.card-author-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  text-align: left;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.3;
}

/* Clickable author name on cards */
.card-author-link {
  background: none;
  border: none;
  padding: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  cursor: pointer;
  text-align: left;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.3;
  transition: color 0.15s;
}
.card-author-link:hover {
  color: var(--primary);
}

/* Musician's own card title reuses .card-author-link for its "make text also a link"
   treatment, but should look like a normal card title (matching Movies etc.), not the
   smaller byline style .card-author-link otherwise uses for the artist name on Album cards. */
.card-author-link.card-title {
  font-size: 20px;
  font-weight: 600;
  color: var(--text-primary);
  -webkit-line-clamp: 1;
}
.card-author-link.card-title:hover {
  color: var(--primary);
}

/* Clickable author name in detail modal */
.detail-author-link {
  display: inline-flex;
  align-items: center;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: color 0.15s;
}
.detail-title-arrow {
  width: 16px;
  height: 16px;
  margin-left: 2px;
  flex-shrink: 0;
}
.detail-author-link:hover {
  color: var(--primary);
}

/* The artist name in a Music Album's combined "Artist | Title" title line, and the Book author
   byline — both purple by default, not just on hover, so they read as a distinct label from the
   (white) title next to them. Dims on hover instead of the base .detail-author-link:hover's
   (redundant, since it's already purple) color change. Other categories' authors keep the plain
   default/hover-purple treatment. */
.detail-album-artist-link,
.detail-book-author-link {
  color: var(--primary);
}
.detail-album-artist-link:hover,
.detail-book-author-link:hover {
  color: var(--primary);
  opacity: 0.6;
}

/* Back button in grid title */
.author-back-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: color 0.15s;
}
.author-back-btn:hover {
  color: var(--primary);
}

.modal-x-close:hover {
  color: var(--text-primary);
}

@media (max-width: 768px) {
  .author-works-grid {
    grid-template-columns: 1fr;
  }
  .author-page-header {
    flex-wrap: wrap;
  }
  /* The website URL moves below the header's own divider here instead of sharing the genre row —
     per direct request ("put the url below the divider line in mobile"). .author-page-website-row
     (the mobile copy, positioned there in the DOM already — renderAuthorPage.js) swaps on;
     .author-page-website--inline (the desktop copy, up in the genre row) swaps off. With the url
     gone, the genre row holds just the pill, so its own separator ("|", only meaningful between
     two things) hides too. */
  .author-page-website-row {
    display: block;
  }
  .author-page-website--inline {
    display: none;
  }
  .author-page-genre-sep {
    display: none;
  }
  .author-page-website-row .author-page-website {
    max-width: 100%;
  }
}


/* ===== TOP 100 CURATED LANDING PAGE =====
   Deliberately distinct from the Dashboard's neutral, utilitarian look — this is meant to feel
   like its own fun destination (and double as a live demo for pitching nonprofit sponsors), even
   though it reuses the Dashboard's .dash-carousel scroll/arrow mechanics under the hood. See
   renderCuratedGenreLanding() in render.js. */

.top100-landing {
  display: block; /* overrides .cards-grid's default grid layout — wins on source order alone,
                      since this rule is declared after .cards-grid in this same file */
  padding-bottom: 24px;
}

.top100-hero {
  position: relative;
  border-radius: 16px;
  padding: 32px 28px;
  margin-bottom: 28px;
  background: linear-gradient(135deg, #5B5BEF 0%, #8B7CF6 55%, #C084FC 100%);
  color: #fff;
}

/* Keeps the text block clear of .top100-icon-badge horizontally instead of relying on vertical
   spacing — the badge sits at left:28px, is 121px wide, so text starts 50px past its right edge
   (28 + 121 + 50 = 199px) rather than sharing the hero's own 28px left padding. */
.top100-hero-text {
  padding-left: 171px;
}

/* A square brand-mark badge, vertically centered on the gradient hero — was previously anchored
   to the hero's bottom edge, LinkedIn-cover-photo/avatar style (half on the banner, half on the
   page below it), moved fully onto the banner per direct request. Solid background (not the
   hero's gradient) since the icon asset itself is plain white and needs a flat backdrop to read
   clearly; the border matches the page background so it still reads as its own distinct mark.
   Horizontally centered in the gap between the hero's left edge and where the text block starts
   (28px hero padding + 171px hero-text padding-left = 199px gap, minus the badge's own 121px
   width, halved) rather than flush against the hero's padding. */
.top100-icon-badge {
  position: absolute;
  left: 39px;
  top: 50%;
  transform: translateY(-50%);
  width: 121px;
  height: 121px;
  border-radius: 18px;
  background: #4638C2;
  border: 4px solid var(--bg);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
  display: flex;
  align-items: center;
  justify-content: center;
}

.top100-icon-badge img {
  width: 66px;
  height: 66px;
  object-fit: contain;
  display: block;
}

.top100-wordmark {
  margin-bottom: 14px;
}

.top100-wordmark-logo {
  height: 24px;
  width: auto;
  display: block;
  opacity: 0.92;
}

.top100-hero-title {
  font-size: 30px;
  font-weight: 800;
  line-height: 1.15;
  margin-bottom: 10px;
}

.top100-hero-desc {
  font-size: 15px;
  line-height: 1.5;
  max-width: 640px;
  /* rgba (not the `opacity` property) so this dims only the base text color — an element's own
     `opacity` is a compositing effect that dims its children too, which would keep
     .top100-hero-desc-link from ever reading as pure white. */
  color: rgba(255, 255, 255, 0.92);
}

.top100-hero-desc-link {
  color: #fff;
  text-decoration: underline;
  font-weight: 600;
}

.top100-hero-desc-link:hover {
  opacity: 0.85;
}

/* Holds the real #sort-select, physically relocated here from its usual .grid-header spot —
   see renderCuratedGenreLanding() in render.js. .grid-header .sort-select's own margin-left:auto
   rule no longer applies once it's moved out from under that ancestor, so it just sits at its
   natural size on the left here. */
.top100-sort-wrap {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 18px;
}

.top100-row {
  margin-bottom: 30px;
}

.top100-row-header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0 0 12px;
  color: inherit;
}

.top100-row-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text-primary);
}

.top100-row-see-all {
  font-size: 13px;
  font-weight: 500;
  color: var(--primary);
  opacity: 0;
  transition: opacity 0.15s;
}

/* Sits right after the row title (not at the far right where "See all" is) — clicking it does
   the exact same thing clicking the row header/title already does (opens that category's full
   curated view, same sidebar-highlighting behavior included), it's just a more visible, always-on
   affordance. No separate click handler needed — it's a child of .top100-row-header, whose click
   listener already covers it via normal event bubbling. Styled to match .kanban-expand-btn
   (kanban.css) exactly, per direct request — same size/border/hover treatment. */
.top100-row-add-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1.5px solid var(--border);
  background: var(--surface);
  color: var(--text-secondary);
  cursor: pointer;
  padding: 0;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.top100-row-add-btn:hover {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

.top100-row-header:hover .top100-row-see-all {
  opacity: 1;
}

.top100-carousel {
  padding: 0 2px;
}

/* .dash-carousel-strip's base 14px gap (dashboard.css) is shared with the Dashboard's own
   carousel — scoped here so widening it (+10px) only affects these Top 100 rows. */
.top100-carousel .dash-carousel-strip {
  gap: 24px;
}

.top100-row-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  background: none;
  border: none;
  cursor: pointer;
  flex-shrink: 0;
  width: 264px;
  scroll-snap-align: start;
}

.top100-row-card-art {
  position: relative;
  width: 264px;
  height: 149px; /* ~16:9, the classic Netflix-row thumbnail shape (label stays below, not overlaid) */
  border-radius: 10px;
  overflow: hidden;
  background: var(--hover-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.top100-row-card-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.top100-row-card:hover .top100-row-card-art {
  transform: translateY(-4px) scale(1.04);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
}

.top100-row-card-label {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.top100-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 12px;
  padding: 20px 24px;
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
}

.top100-cta-text {
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
}

.top100-cta-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--primary);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: background 0.15s;
}

.top100-cta-btn:hover,
.top100-cta-btn:active {
  background: #fff;
  color: var(--primary);
}

@media (max-width: 768px) {
  /* .top100-icon-badge is normally absolutely positioned + vertically centered mid-hero, with
     .top100-hero-text given a fixed 171px left-padding to clear it horizontally (see both rules'
     own comments above) — a layout that assumes a wide, short hero. On mobile the same fixed
     padding leaves almost no room for the text, so it wraps severely and the hero grows tall to
     fit it; the badge, still centered on that now-tall hero, ends up floating mid-paragraph
     (reported as "the Votecraft page looks messed up" on an iPhone). Stack the badge above the
     text in normal flow instead, both shrunk down to fit comfortably. column-reverse puts it
     first visually without needing to reorder the HTML (badge is the second child in the DOM). */
  .top100-hero {
    padding: 24px 20px;
    display: flex;
    flex-direction: column-reverse;
    gap: 14px;
  }
  .top100-hero-text {
    padding-left: 0;
  }
  .top100-icon-badge {
    position: static;
    transform: none;
    width: 56px;
    height: 56px;
    border-radius: 12px;
  }
  .top100-icon-badge img {
    width: 32px;
    height: 32px;
  }
  .top100-hero-title {
    font-size: 24px;
  }
}

/* ===== CURATED SAVECRAFT DIRECTORY =====
   The top-level "Curated SaveCraft" landing (no genre picked yet) — a fully inert visual pitch/
   demo of many nonprofit-sponsored lists, ActBlue-style cause groupings. Reuses .top100-hero/
   .top100-cta above for the header/CTA band; only the category rows below are new. Cards have no
   hover-to-navigate affordance (cursor: default) since nothing here is clickable. */

.directory-category {
  margin-bottom: 28px;
}

.directory-category-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 12px;
}

/* Sized to exactly match .top100-row-card's thumbnail (264x149, ~16:9) so this directory's
   sliders read as the same "Netflix row" shape as the Top 100 landing page's — art box on top,
   text below it, not the earlier smaller icon-chip card. Not reusing .top100-row-card directly
   since these cards are inert (no hover-lift/cursor-pointer, no click wiring) and show an emoji
   in place of real cover art. */
.directory-org-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  width: 264px;
  flex-shrink: 0;
  scroll-snap-align: start;
  cursor: default;
  text-align: left;
}

.directory-org-art {
  width: 264px;
  height: 149px;
  border-radius: 10px;
  background: var(--hover-bg);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
}

.directory-org-logo {
  width: 72px;
  height: 72px;
  object-fit: contain;
}

/* Same fix as .bare-list-avatar img[src*="votecraft_icon_white"] above — the VoteCraft app icon
   PNG has a lot of built-in canvas padding, so it reads smaller than the other logos at 72px. */
.directory-org-logo[src*="votecraft_icon_white"] {
  width: 82px;
  height: 82px;
}

.directory-org-icon {
  font-size: 48px;
  line-height: 1;
}

.directory-org-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
}

.directory-org-tagline {
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text-secondary);
}

/* ===== CURATED SAVECRAFT — BARE-BONES TOP-LEVEL LIST =====
   The default "Curated SaveCraft" landing (before picking a genre) — a plain, ActBlue-style flat
   list of the same nonprofit-sponsored orgs as the fuller "Curated-full-list" hero+carousel page
   above, with a link through to that richer page. Deliberately spare: no hero band, no carousels,
   just a filter rail + a scannable list of rows. */

.bare-list-page {
  display: block; /* overrides .cards-grid's default 2-column grid, which was squeezing this
                      single-child page into just the first column's width — same fix as
                      .top100-landing above. */
  padding-left: 40px;
  padding-bottom: 24px;
}

.bare-list-page-inner {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.bare-list-header {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Same gradient as .top100-hero (the VoteCraft Picks / Curated-full-list page), just a much
   shorter strip — a small background banner behind the title/description, not a full hero with
   its own wordmark/icon badge. */
.bare-list-hero {
  background: linear-gradient(135deg, #5B5BEF 0%, #8B7CF6 55%, #C084FC 100%);
  border-radius: 16px;
  padding: 14px 20px;
  align-items: center;
  text-align: center;
  margin-left: -20px;
  /* Matches margin-left, per request — was missing entirely, so the banner sat 20px further out
     on its left edge than its right, an asymmetry that showed up on both pages this shared
     component is used on (Cause Curated and Shared Saves). */
  margin-right: -20px;
}

/* Cause Curated only — per direct request, this page's banner text is left-aligned instead of
   the shared .bare-list-hero's own centered default (Shared Saves, which reuses this same class,
   keeps center). */
.bare-list-page .bare-list-hero {
  text-align: left;
  align-items: flex-start;
}

.bare-list-title {
  font-size: 22px;
  font-weight: 700;
  color: #fff;
  margin: 0;
}

.bare-list-desc {
  font-size: 14px;
  color: rgba(255, 255, 255, 0.92);
  margin: 0;
}

.bare-list-seeall-btn {
  align-self: flex-start;
  margin-top: 16px;
  padding: 10px 20px;
  border-radius: 8px;
  border: none;
  background: var(--primary-bg);
  color: var(--primary);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

.bare-list-seeall-btn:hover {
  background: var(--primary);
  color: #fff;
}

.bare-list-body {
  display: flex;
  gap: 32px;
  align-items: flex-start;
}

.bare-list-filters {
  flex-shrink: 0;
  width: 290px;
  margin-top: 40px;
  margin-left: 50px;
}

.bare-list-filter-section-title {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-secondary);
  margin-bottom: 10px;
}

.bare-list-why-title {
  margin-top: 80px;
}

/* "Why Curated Lists" is now itself the accordion's clickable header (a <button>, was a plain
   <div>), per request — resets the button-specific defaults browsers add (border/background/
   font-family/text-align) so it still reads exactly like the other section titles, plus lays out
   its own label + arrow icon side by side. */
.bare-list-accordion-toggle {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  border: none;
  background: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.bare-list-accordion-arrow {
  flex-shrink: 0;
  transition: transform 0.15s;
}
.bare-list-accordion-toggle--open .bare-list-accordion-arrow {
  transform: rotate(180deg);
}

.bare-list-why-copy {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-secondary);
  margin: 0;
}

.bare-list-chips {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}
/* Real bug: .bare-list-chips-more (the "View more" toggle's extra chips) carries this same
   .bare-list-chips class, so its own display: flex above was silently overriding the browser's
   built-in [hidden] { display: none } — an author-stylesheet rule always wins over a user-agent
   one regardless of specificity, so the hidden attribute never actually did anything and the
   extra chips stayed visible even when collapsed. This re-asserts display: none at high enough
   specificity (class + attribute) to actually win. */
.bare-list-chips-more[hidden] {
  display: none;
}
/* .bare-list-chips-more is a second, separate flex container stacked directly below the first
   5-chip .bare-list-chips — each has its own internal gap: 8px between its own chips, but nothing
   enforces that same spacing *between* the two containers, so the last chip of the first group and
   the first chip of the second sat flush against (visually overlapping) each other once expanded. */
.bare-list-chips-more {
  margin-top: 8px;
}

.bare-list-chip {
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: var(--primary-bg);
  color: var(--primary);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s;
}

.bare-list-chip:hover {
  background: var(--hover-bg);
}

.bare-list-chip--active {
  background: var(--primary);
  color: #fff;
}

/* "View more"/"View less" toggle for the chip list beyond the first
   BARE_LIST_CHIPS_COLLAPSED_COUNT (renderCuratedPages.js) — a plain text link rather than a pill
   like the chips themselves, so it doesn't read as another filter option. .bare-list-chips-more
   (the extra chips it reveals) reuses .bare-list-chips' own column/gap styling as-is; [hidden]
   just removes it from flow until toggled. */
.bare-list-chips-toggle {
  display: block;
  margin-top: 4px;
  padding: 0;
  border: none;
  background: none;
  color: var(--primary);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.bare-list-chips-toggle:hover {
  text-decoration: underline;
}

.bare-list-rows {
  flex: 1;
  min-width: 0;
  max-width: 50%; /* narrowed another 20 points past the earlier 70% */
  margin-left: 160px; /* extra space between the filter rail and the list, on top of .bare-list-body's own gap */
  margin-top: 40px;
  display: flex;
  flex-direction: column;
}

.bare-list-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 20px 36px;
  border-radius: 10px;
  background: var(--hover-bg);
  margin-bottom: 12px;
  cursor: pointer;
  transition: background 0.15s, transform 0.15s, box-shadow 0.15s;
}

/* Demo-only bookmark toggle (see the click handler in renderCuratedBareList()) — appears on row
   hover, no circle/background behind the icon per direct request, turns purple when active. */
.bare-list-bookmark-btn {
  display: none;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 14px;
  right: 16px;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: none;
  color: #9AA0A6;
  cursor: pointer;
  z-index: 1;
}

.bare-list-row:hover .bare-list-bookmark-btn {
  display: flex;
}

.bare-list-bookmark-btn:hover {
  color: var(--primary);
}

.bare-list-bookmark-btn--active {
  display: flex;
  color: var(--primary);
}

.bare-list-row:hover {
  background: var(--border);
  transform: translateY(-2px);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
}

.bare-list-row:hover .bare-list-org-name {
  color: var(--primary);
  text-decoration: none;
}

.bare-list-avatar {
  flex-shrink: 0;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40px;
  color: #fff;
  overflow: hidden;
}

/* Real logos fill the whole circle (matching the Shared Saves slider's avatar treatment) — except
   the VoteCraft icon below, which keeps its own contained/scaled sizing since it's a flat
   white-on-transparent glyph, not a photo/logo suited to a cropped full-bleed fill. */
.bare-list-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The VoteCraft app icon PNG has a lot of built-in canvas padding around the glyph, so it reads
   noticeably smaller than the other orgs' logo images at the same box size — sized up to match. */
.bare-list-avatar img[src*="votecraft_icon_white"] {
  width: 62%;
  height: 62%;
  object-fit: contain;
}

.bare-list-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.bare-list-org-name {
  font-size: 15px;
  font-weight: 600;
  color: #fff;
}

.bare-list-org-tagline {
  font-size: 12.5px;
  line-height: 1.4;
  color: #C4C4CC;
}

.bare-list-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
  margin-bottom: 10px;
}

.bare-list-tag {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--text-primary);
  color: var(--bg);
  font-size: 11px;
  font-weight: 500;
}

.bare-list-tag--muted {
  background: rgba(91, 91, 239, 0.18);
  color: var(--primary);
}

.bare-list-view-btn {
  flex-shrink: 0;
  margin-left: 40px;
  padding: 8px 18px;
  border-radius: 999px;
  border: none;
  background: var(--primary);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  cursor: default;
}

@media (max-width: 768px) {
  /* Narrower than the grid column's full width so the fixed A-Z jump index rail (azIndexRail.js,
     below) — a fixed overlay pinned just inside .grid-area's own right padding — doesn't sit on
     top of the card's own tag pill/text at its right edge. Per direct request ("make it so they
     are a little less wide so that the letters don't overlap the cards"). */
  .cards-grid .card {
    margin-right: 22px;
    /* .card is a fixed height: 110px (desktop) with .card-body's own overflow: hidden — a title
       that now wraps to a real second line (below) needs the card to grow to fit it instead of
       that second line just getting silently clipped off. min-height keeps every other (1-line)
       card at its normal compact size. */
    height: auto;
    min-height: 110px;
  }
  /* REAL BUG, found and fixed: .card's height above went from a fixed 110px to auto — but
     .card-image-crop is a flex item sized via height: 100% (cards.css), and a percentage height
     on a flex item is spec'd to resolve as 'auto' whenever its flex container's own height is
     itself indefinite/auto (exactly the circular case this now created). For an 'auto' height,
     an <img> (a replaced element) falls back to its own intrinsic aspect ratio at its fixed
     100px width instead of stretching to match the row — so every card's image (and, via the
     stretch algorithm, the whole card) ended up sized to whatever THAT PHOTO's own aspect ratio
     happened to be, wildly inconsistent card-to-card (reported live, screenshotted: portrait
     photos producing much taller cards than landscape ones, all on the same list — confirmed
     fine on desktop, where .card's height is still a fixed 110px there with no such ambiguity).
     Fix: take the image wrapper out of flex flow entirely (absolute, pinned to .card's own
     padding box via position:relative — already set — with top/left/bottom all literal 0s, not
     percentages) so it can no longer influence the row's height calculation at all — same
     already-proven technique .detail-image-crop (detailModal.css) uses for the same reason.
     .card's own height is then driven purely by .card-body's natural content, and the image
     simply fills whatever box results. .card-body gets a matching margin-left so its text no
     longer renders underneath the now out-of-flow image. */
  .card-image-crop {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    width: 100px;
    height: auto;
  }
  .card-body {
    margin-left: 100px;
  }
  /* 20px -> 16px (20% smaller), per direct request ("make the card title font 20% smaller...
     in mobile that is"/"only mobile") — desktop's 20px is untouched. Also allowed to wrap to a
     second line here (1 -> 2), per direct follow-up ("in the other sections i also want the
     mobile card text to be able to be on two lines") — a long title truncating with an ellipsis
     after just one line reads worse on a narrow phone screen than a real second line does. */
  .card-title {
    font-size: 16px;
    -webkit-line-clamp: 2;
  }
  /* .card-author-link.card-title (Musician/other creator-card titles — cards.css) is a 2-class
     selector, so it out-specifies the plain 1-class .card-title rule above regardless of source
     order — its own desktop font-size: 20px was silently winning here too, so the shrink above
     never actually reached these cards. Per direct follow-up ("make font in the music cards in
     mobile the same size you are using on the other sections"). Left at its own 1-line clamp
     (not the 2-line change above) — that follow-up was scoped to "the other sections". */
  .card-author-link.card-title {
    font-size: 16px;
  }
  /* 11px -> 9px (20% smaller, rounded), per direct follow-up ("in mobile make the card tags 20%
     smaller") — desktop's 11px is untouched. */
  .card-badge {
    font-size: 9px;
  }

  /* Was padding-left: 40px only (no padding-right) — fine on desktop where .bare-list-rows'
     own max-width: 50% + margin-left: 160px kept everything well clear of the right edge anyway,
     but on mobile (rows now full-width, right below) that left the row list's own right edge
     flush against the real screen edge with zero margin, squeezing it further than the fix below
     alone accounts for. Symmetric padding instead. */
  .bare-list-page {
    padding: 0 16px 24px;
  }
  .bare-list-body {
    flex-direction: column;
  }
  .bare-list-filters {
    width: 100%;
    margin-left: 0;
    margin-top: 20px;
  }
  .bare-list-why-title {
    margin-top: 24px;
  }
  /* This part of the fix was previously missing: .bare-list-body switching to a column above
     stacks the filter rail above the list, but .bare-list-rows itself still kept its desktop
     max-width:50% + margin-left:160px (sized to sit beside the filter rail, not below it) — so
     the row list stayed squeezed into half the screen and shoved right, severely word-wrapping
     every row's text (reported live on an iPhone: "same with these curated lists"). */
  .bare-list-rows {
    max-width: 100%;
    margin-left: 0;
    margin-top: 20px;
  }
  /* Restructured as a grid, not a single-line flex row — squeezing avatar + title + tagline + tag
     + the View button all onto shared horizontal space (previous approach) left the text column
     only ~140px wide on a real iPhone even after shrinking the avatar and pulling the button out
     to an absolute corner position, wrapping a one-sentence tagline across 5-6 lines (reported
     live, screenshotted). Only the title now shares a line with the avatar; the tagline and tag
     each get their own full-width row below, which is what a phone-width card actually needs
     room for. .bare-list-info's display: contents "unwraps" it so its own children (name/
     tagline/tags) become direct grid items here instead of one combined grid cell — modern
     browsers only (iOS 15+), already the norm this codebase relies on elsewhere (:has(), etc). */
  .bare-list-row {
    display: grid;
    grid-template-columns: 56px 1fr;
    column-gap: 14px;
    row-gap: 4px;
    grid-template-areas:
      "avatar name"
      "tagline tagline"
      "tags tags";
    align-items: start;
    /* Right padding leaves room for the disclosure arrow (::after, below); not the old 52px
       bottom reservation the View button used to need — the whole row is already tappable via
       its own data-link-to click handler, so the button was redundant on mobile anyway, per
       request. */
    padding: 16px 34px 16px 20px;
  }
  /* Right-pointing chevron, vertically centered against the row's own full height — replaces the
     removed View button as the "this row is tappable" affordance, per request. Drawn from a
     rotated border corner rather than an image/icon asset. */
  .bare-list-row::after {
    content: '';
    position: absolute;
    top: 50%;
    right: 18px;
    width: 8px;
    height: 8px;
    border-top: 2px solid var(--text-secondary);
    border-right: 2px solid var(--text-secondary);
    transform: translateY(-50%) rotate(45deg);
  }
  .bare-list-info {
    display: contents;
  }
  /* Avatar shrunk (was the full 96px desktop size, still too big once next to nothing but the
     title now) and given its own grid area rather than sizing itself via flex. */
  .bare-list-avatar {
    grid-area: avatar;
    width: 56px;
    height: 56px;
    font-size: 22px;
  }
  /* Clamped to a fixed number of lines (title 1, tagline 2), per request — org names/taglines
     vary a lot in length ("Pop culture with a civic pulse." vs. "Getting the money out of
     politics, one story at a time."), so without a cap each card's own height varied with however
     much its own text happened to wrap, reading as an uneven, unaligned list. Longer text now
     truncates with an ellipsis instead of growing the card. */
  .bare-list-org-name {
    grid-area: name;
    align-self: center;
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .bare-list-org-tagline {
    grid-area: tagline;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Reserves 2 full lines' worth of height even when the actual text is shorter — line-clamp
       above only caps the *maximum*, it doesn't stop a one-line tagline from leaving the card
       shorter than a two-line one. This is what actually makes every card's height match. */
    min-height: calc(1.4em * 2);
  }
  .bare-list-tags {
    grid-area: tags;
    /* Zeroed — the default 10px top/bottom (desktop rule) doubled up with row-gap: 4px above,
       and this grid already controls spacing between rows on its own. */
    margin-top: 0;
    margin-bottom: 0;
  }
  /* Hidden on mobile, per request — the whole row is already tappable (data-link-to's own click
     handler in renderCuratedPages.js), so the button was redundant, and removing it frees up the
     row's own bottom padding above (was reserving 52px just for this). */
  .bare-list-view-btn {
    display: none;
  }
}

/* ===== A-Z INDEX RAIL ===== */
/* position: fixed, repositioned in JS against .grid-area's own live rect (azIndexRail.js) — see
   its own comment for why sticky isn't a fit here. Narrow/unobtrusive since it overlays the
   rightmost edge of whatever card is beneath it rather than reserving its own column. */
.az-index-rail {
  position: fixed;
  z-index: 15;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 4px 3px;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.az-index-rail[hidden] {
  display: none;
}

.az-index-letter {
  background: none;
  border: none;
  padding: 1.5px 3px;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.25;
  color: var(--primary);
  cursor: pointer;
}

.az-index-letter:hover,
.az-index-letter:active {
  color: #fff;
  background: var(--primary);
  border-radius: 3px;
}
