/* ---- detail view ---- */

/* Now a compact button sitting at the LEFT edge of .detail-top-bar rather
   than a standalone line above the page -- left because it's the "exit"
   control, same side as the carousel's own prev/back-in-time arrow.
   Colored with the app's existing red (--tier-4x, already used for
   banned-tag/hardcore-mode/4x-weakness elsewhere) rather than the usual
   accent blue, so it doesn't read as just another carousel control. */
.detail-back {
  color: #ffc9c4;
  display: inline-block;
  white-space: nowrap;
  padding: 8px 12px;
  border: 1px solid var(--tier-4x);
  border-radius: 8px;
  background: color-mix(in srgb, var(--tier-4x) 28%, transparent);
  flex-shrink: 0;
}
.detail-back:hover { background: var(--tier-4x); color: #fff; }

/* Sticky bar pinned below the app header with a permanent gap, replacing
   the old standalone "Back to Pokedex" link at the top of the page. Houses
   the relocated back button (left) and the Pokemon carousel (right), both
   riding along as the moveset table scrolls underneath -- see
   .detail-left-col below, whose own sticky offset is pushed down to make
   room for this bar stacking above it.

   `top` is --app-header-h (measured by main.js) PLUS a deliberate 18px gap
   -- chosen to match #app-root's own top padding, which is exactly what
   used to put that same 18px of daylight between the header and this bar
   before any of this sticky math existed. The key point: this gap is now
   baked into the STICKY THRESHOLD itself, not left as a side effect of
   #app-root's padding the way it was originally. Since the bar's natural
   (unscrolled) resting position is ALSO header-height + 18px (that's just
   where it falls in normal flow, sitting right after #app-root's own top
   padding), the natural position and the sticky threshold are now the same
   number -- so there's no travel distance for the bar to visibly slide
   through before locking, but the 18px gap itself never closes, at rest or
   once stuck. (An earlier attempt fixed the sliding by cancelling the gap
   out entirely with a negative margin, which also removed the gap the user
   actually wanted to keep -- this version keeps both.)

   No fade/mask elements around this bar anymore -- an extended attempt at
   softening the seam above it and the handoff to the moveset table below
   caused more visible problems (eaten rounded corners, inconsistent
   left/right column behavior) than the hard edges it was meant to fix.
   Rolled back; the bar just has a plain hard edge on all sides now. */
.detail-top-bar {
  position: sticky;
  top: calc(var(--app-header-h, 72px) + 18px);
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 16px;
  /* 10px, matching the "+10px"/"-10px" bar-gap term in .detail-left-col/
     .detail-right-col's own top/max-height below -- these two numbers have
     to stay equal. This margin is what determines where those columns
     naturally sit before any scrolling happens; their sticky `top` is a
     separate, independently-computed number for where they lock once
     scrolled. If the two ever disagree, the columns' resting position and
     their scroll-locked position stop matching, which is exactly what
     caused the bar/panel "wobble" bug earlier in the session (a visible
     slide into place instead of standing still). */
  margin-bottom: 10px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 10px;
}

.pokemon-carousel { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }

.carousel-arrow {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-panel-alt);
  color: var(--text);
  font-size: 1rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.carousel-arrow:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.carousel-arrow:disabled { opacity: 0.3; cursor: default; }

/* Fixed-width track, evenly splitting its own space across however many
   name slots actually rendered (2 neighbors each side, fewer right at the
   start/end of the list) -- names are size/opacity-graded by distance from
   center via the carousel-name-* classes below, plus a soft edge-mask so
   the two outermost slots trail off rather than cutting off sharply. */
.carousel-track {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-evenly;
  gap: 6px;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 12%, #000 88%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 12%, #000 88%, transparent 100%);
}

.carousel-name {
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: center;
  transition: color 0.15s ease;
}
.carousel-name:hover { color: var(--accent); }
/* Overrides the app-wide :focus-visible box outline (see base.css) just
   for these -- arrow-key stepping through the carousel refocuses a new
   name every keypress, and a rectangular ring popping in and out around
   short inline text read as heavy/boxy for something this frequent.
   Underline is the same "keyboard focus is here" signal without a drawn
   box; offset+thickness keep it from crowding descenders in the name. */
.carousel-name:focus-visible {
  outline: none;
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
/* max-width is set per distance class rather than shared, since the
   current slot's bigger font fits noticeably fewer characters in the same
   pixel width than the smaller near/far ones do -- a single shared
   max-width was truncating the centered, most-important name while its
   smaller neighbors sailed by untruncated, backwards from how it should
   read. Current gets the most room; far gets the least since it's already
   faded down to barely-there anyway. */
.carousel-name-current { font-size: 1.05rem; font-weight: 700; color: var(--text); opacity: 1; cursor: default; max-width: 220px; }
.carousel-name-current:hover { color: var(--text); }
.carousel-name-near { font-size: 0.85rem; color: var(--text-dim); opacity: 0.7; max-width: 130px; }
.carousel-name-far { font-size: 0.75rem; color: var(--text-dim); opacity: 0.4; max-width: 100px; }

@media (max-width: 900px) {
  .carousel-name-far { display: none; }
}

/* Left column widened from its old 460px cap -- the moveset table's
   description cell (.move-desc, right column) had more max-width than its
   text needed, so that slack has been moved over here instead. Grid gap
   widened from 16px to make room for the scrollbar to sit visibly clear of
   both stacks rather than cramped right against the left one. */
.detail-columns { display: grid; grid-template-columns: minmax(320px, 520px) 1fr; gap: 28px; align-items: start; }

/* Both columns are self-contained, independently scrolling boxes --
   .detail-right-col (the moveset table) used to just be normal page-flow
   content, with the whole PAGE scrolling to reveal it. That caused two
   problems: content had to visually pass "underneath" the sticky bar,
   which is what all of the fade/masking work (since rolled back) was
   trying to paper over; and since .detail-columns (this grid) stretched to
   match the moveset table's full, un-clipped length, .detail-left-col --
   sticky, but still bound by ITS container's own height -- would run out
   of room near the bottom of a long moveset and get pushed up past its own
   locked position, covering the shiny toggle. Capping BOTH columns to the
   same viewport-relative height with their own internal scroll fixes both
   at once: nothing ever needs to pass behind the bar, and .detail-columns
   itself never grows taller than roughly one viewport, so .detail-left-col
   never runs low on room to push against.

   Offset pushed down to clear both the app header AND .detail-top-bar
   (including its own permanent 18px gap above it, see .detail-top-bar's
   own `top` for why). Built from the same --app-header-h/--carousel-bar-h
   variables the bar itself uses (see main.js/detail.js), rather than a
   hardcoded guess that could drift out of sync with the bar's own actual
   offset -- the +10px accounts for the bar's own margin-bottom gap
   underneath it (must stay equal to that margin -- see the comment on
   .detail-top-bar's own margin-bottom for why). Fallback values (72px/76px)
   keep this sane before either variable has been measured for the first
   time.

   max-height's trailing subtraction is just header + gap + bar + 10px,
   with NO extra bottom buffer -- the box's bottom edge sits flush with the
   bottom of the browser window instead of leaving a visible strip of empty
   page background beneath it.

   Both boxes are 10px taller at the top than they visually appear to be at
   rest -- padding-top (below) reserves that extra 10px as empty space
   directly inside the box, pushing the real first panel down by the same
   10px so it lands exactly where it always has. That reserved strip only
   becomes visible once you actually scroll content up through it, at which
   point the mask-image fade (below) applies -- since it's genuinely empty
   at rest, there's nothing there to fade prematurely, so unlike an earlier
   attempt at this same idea, this fade can just always be on rather than
   needing a scroll-triggered toggle. */
.detail-left-col,
.detail-right-col {
  position: sticky;
  top: calc(var(--app-header-h, 72px) + 18px + var(--carousel-bar-h, 76px) + 10px);
  max-height: calc(100vh - var(--app-header-h, 72px) - 18px - var(--carousel-bar-h, 76px) - 10px);
  overflow-y: auto;
  padding-top: 10px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 10px, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 10px, #000 100%);
  /* Without this, once a column's own inner scroll reaches its end, the
     browser "scroll-chains" further wheel input up to the page instead of
     just stopping -- which read as one column's scroll bleeding into
     whatever's next in the document. Containing it here keeps both
     columns' scrolling fully independent of each other and of the page. */
  overscroll-behavior: contain;
}
.detail-left-col {
  display: flex;
  flex-direction: column;
  gap: 16px;
  /* The scrollbar renders at this column's own right edge, INSIDE this
     padding -- it was covering the panels' own border when this was only
     4px (barely any buffer between panel content and the scrollbar).
     Widened so the scrollbar sits with clear space on both sides: this
     padding separates it from the panel border on the left, and the grid
     gap above separates it from the moveset panel on the right, landing
     it roughly centered in the visual gap between the two stacks. */
  padding-right: 16px;
}
.detail-right-col {
  min-width: 0;
  /* Same reasoning as .detail-left-col's own padding-right above -- gives
     this column's scrollbar (now that it has one of its own) breathing
     room from its content instead of sitting flush against it. */
  padding-right: 16px;
}
@media (max-width: 900px) {
  .detail-columns { grid-template-columns: 1fr; }
  .detail-left-col,
  .detail-right-col {
    position: static;
    max-height: none;
    overflow: visible;
    /* The top-padding/mask trick above only makes sense for the capped,
       independently-scrolling boxes used at wider widths -- at this
       breakpoint both columns are back to plain page flow, so neither the
       reserved empty space nor the fade are needed here. */
    padding-top: 0;
    -webkit-mask-image: none;
    mask-image: none;
  }
}

.detail-header {
  position: relative;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px;
}

/* Shiny toggle now lives in the box's own top-right corner rather than
   inline on the ID line -- it was pushed out of bounds there by longer
   form badges, and sitting alone in the corner reads more like a page
   control than part of the ID text anyway. A text label sits to its left
   so it's clear what the toggle actually does at a glance, same as every
   other toggle in the app (see .ios-toggle-wrap). */
.detail-shiny-toggle-corner { position: absolute; top: 14px; right: 14px; display: flex; align-items: center; gap: 8px; }
.detail-shiny-toggle-label { font-size: 0.8rem; color: var(--text-dim); }

/* Mirrors the shiny toggle's own corner, on the opposite side, so the two
   "in line with each other" controls the favoriting request asked for
   actually read as a matched pair. */
.detail-favorite-corner { position: absolute; top: 14px; left: 14px; display: flex; align-items: center; }

/* One star glyph everywhere (see favoriteStarHtml in helpers.js) -- gray
   by default, yellow-orange once favorited. Color alone carries the state
   rather than swapping glyphs, so there's only one piece of markup for
   the detail page's corner button and the dex's small inline star to
   share. */
.favorite-star {
  border: none; background: transparent; cursor: pointer; padding: 0;
  font-size: 1.5rem; line-height: 1; color: var(--text-dim);
}
.favorite-star:hover { color: var(--text); }
.favorite-star-active, .favorite-star-active:hover { color: #f5a623; }
/* The dex's inline star, next to a Pokemon's name -- sized to sit level
   with lowercase text rather than towering over the name like the detail
   page's own corner button does. */
.favorite-star-small { font-size: 0.8rem; vertical-align: middle; }
/* The dex Matrix card's own corner star -- sits inside .dex-card-corner-right's
   own flex column, directly below BST, rather than being positioned on its
   own -- the wrapper handles the top/right placement. */
.favorite-star-corner { font-size: 0.9rem; line-height: 1; }
/* Mirrors .dex-card-corner's own nudge on .form-badge (see comment there) --
   the star glyph carries its visual weight high in its own box the same way
   the tightly-padded form badge does, so the bare flex `gap` alone left it
   reading closer to the BST above it than the form badge (its visual match
   on the opposite corner) reads to its own #ID. This centers the star
   vertically against the form badge rather than just matching the BST's
   geometric gap. 3px pushed it visibly below the form badge's own center
   (measured against the badge's glyph, not its padded box) -- 1px is what
   actually lines the two up. */
.dex-card-corner-right .favorite-star-corner { margin-top: 1px; }

/* flex-end (not flex-start) so the title block's bottom -- the type badge
   row -- lines up with the bottom of the sprites next to it, instead of
   both blocks just sharing a top edge. justify-content:center treats the
   sprites + title as one compact lumped group and centers that whole
   group within the panel's width -- this only actually centers anything
   because .detail-title (below) no longer stretches with flex:1 to fill
   the remaining width; without that change this row would already span
   edge-to-edge and centering would have nothing to do. */
.detail-header-top { display: flex; gap: 32px; align-items: flex-end; justify-content: center; opacity: 1; transition: opacity 0.12s ease; }
/* Held here (name/types text included, not just the sprites) until the
   fresh front+back sprite images for whatever Pokemon was just navigated
   to have actually loaded -- see updateSprites() in detail.js. Without
   this the name/types paint immediately (they're plain text, no request
   needed) while the sprites are still a beat behind fetching, which reads
   as the name "flashing" in ahead of its own sprite -- worst when stepping
   through the carousel quickly, since every step fires a fresh pair of
   image requests. */
.detail-header-top.sprites-loading { opacity: 0; }

/* Sprite is the primary showcase element. The front/back boxes are a FIXED
   size shared by every species (see FRONT_BASE_PX/BACK_BASE_PX in
   detail.js) -- the image inside is what scales per-species (via its own
   independent front/back factor from build/build_sprite_scale.py), sized
   and positioned absolutely by detailSpriteImgStyle() in helpers.js so a
   species whose actual drawn body only fills part of the 64x64 canvas
   (Bulbasaur, Diglett) still reads at roughly the same visual size as one
   that fills the canvas completely (Charizard, Venusaur), cropped to the
   box by overflow:hidden and ground-aligned via that same helper's bottom
   offset. No background here -- the box should be visually invisible,
   just a crop window, not its own separate panel-within-a-panel. */
.sprite-col { flex-shrink: 0; text-align: center; }
.sprite-row { display: flex; gap: 8px; align-items: flex-end; }
.detail-sprite-box { overflow: hidden; position: relative; border-radius: 8px; }
.detail-sprite { image-rendering: pixelated; display: block; color: transparent; }

/* No longer flex:1 -- that stretched this block to fill all remaining
   panel width, which is what kept the sprites+title row from ever reading
   as one centered group (it always spanned edge-to-edge). Now sized to
   its own content, capped so a long name/type row still wraps within the
   panel instead of pushing the group wider than the panel itself. */
.detail-title { min-width: 0; max-width: 320px; }
.dex-id-line { display: flex; align-items: center; gap: 8px; color: var(--text-dim); font-size: 0.95rem; margin-bottom: 2px; }
.dex-id-line .form-badge { font-size: 0.68rem; padding: 2px 6px; }
.detail-title h1 { margin: 2px 0 6px; font-size: 1.7rem; line-height: 1.1; }
.detail-title .dex-types { display: flex; gap: 6px; margin: 8px 0; flex-wrap: nowrap; }
.detail-title .dex-types .type-badge { font-size: 0.85rem; padding: 3px 11px; white-space: nowrap; }

.abilities-block { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); }
/* Fixed 2x2 grid, NOT the old vertical list -- a Pokemon has 1-3 abilities,
   so that list used to be 1-3 rows tall, making this whole header panel
   grow/shrink while stepping through the detail carousel. Both the row
   height AND the column split are fixed regardless of content, so the
   panel's total height is identical on every Pokemon's page: top-left/
   top-right/bottom-left are ability slots (blank if this Pokemon has fewer
   than 3 -- see renderAbilitiesBlock), bottom-right is always Generation,
   ability count or not. */
.abilities-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: repeat(2, 64px);
  gap: 8px 16px;
}
.ability-cell { min-width: 0; overflow-y: auto; }
.ability-cell-empty { visibility: hidden; }
.ability-entry-name { display: flex; align-items: center; font-weight: 700; font-size: 0.95rem; cursor: pointer; }
/* Only the name text itself reacts to hover (underline + brighten, same
   treatment as move names) -- targeting the inner span rather than the
   whole .ability-entry-name row keeps the (HIDDEN) tag next to it from
   picking up the same hover styling, which read as if the tag itself were
   also clickable/interactive when it isn't. */
.ability-entry-name:hover > .ability-name-text { text-decoration: underline; filter: brightness(1.25); }
.ability-entry-name.hidden-ability .ability-name-text { color: #e79bd6; }
.ability-entry-name .hidden-tag {
  margin-left: 10px;
  font-size: 0.62rem;
  font-weight: 700;
  color: var(--text-dim);
}
.ability-entry-desc { font-size: 0.8rem; color: var(--text-dim); margin-top: 2px; }
.generation-cell { display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-end; text-align: right; }
.generation-cell-title { font-weight: 700; font-size: 0.95rem; }
.generation-cell-subtitle { font-size: 0.8rem; color: var(--text-dim); margin-top: 2px; }

.panel {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px;
}

.panel h2 { margin: 0 0 12px; font-size: 1.05rem; }
.panel h3 { margin: 14px 0 8px; font-size: 0.9rem; color: var(--text-dim); }

/* Info/credits page: the back link used to sit flush against the first
   panel below it, and every panel after that sat flush against its own
   neighbor too, with nothing but each panel's own internal padding to
   separate them. `.credits-page` groups everything below the back link
   into one column with a real gap between every child -- the back link
   itself stays outside this wrapper (it's a single inline-block link, not
   a panel) so its own spacing from the group is set separately. */
.detail-back + .credits-page { margin-top: 16px; }
.credits-page { display: flex; flex-direction: column; gap: 16px; }
.credits-page p { margin: 0 0 10px; }
.credits-page p:last-child { margin-bottom: 0; }
.credits-page ul { margin: 0 0 10px; padding-left: 20px; }
.credits-page ul:last-child { margin-bottom: 0; }
.credits-page li { margin-bottom: 6px; }
.credits-page li:last-child { margin-bottom: 0; }
.credits-page a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
.credits-page a:hover { filter: brightness(1.2); }
/* Holds the self-built repo card (see credits.js's renderRepoEmbed, which
   fetches the repo's public GitHub API data and fills this in) -- empty
   until that fetch resolves, so it's given a min-height up front to avoid
   the panel visibly collapsing and then jumping taller once the card
   actually appears. */
.credits-repo-embed { max-width: 420px; min-height: 80px; margin: 4px 0 12px; }
.credits-repo-card {
  display: flex; flex-direction: column; gap: 6px;
  padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg-panel-alt);
}
.credits-repo-card-name {
  display: flex; align-items: center; gap: 6px;
  font-weight: 700; font-size: 0.92rem; word-break: break-all;
  color: var(--text) !important; text-decoration: none !important;
}
.credits-repo-card-name:hover { color: var(--accent) !important; }
.credits-repo-card-name svg { width: 16px; height: 16px; flex: none; }
.credits-repo-card-desc { margin: 0; font-size: 0.8rem; color: var(--text-dim); }
.credits-repo-card-stats {
  display: flex; align-items: center; flex-wrap: wrap; gap: 14px;
  font-size: 0.78rem; color: var(--text-dim);
}
/* Every stat (language dot+name, star count, fork count) is its own flex
   row rather than relying on inline-element baseline alignment -- that's
   what threw the language dot/text off-center in the third-party widget
   this replaced. */
.credits-repo-card-stat { display: flex; align-items: center; gap: 5px; }
.credits-repo-card-stat svg { width: 14px; height: 14px; flex: none; }
.credits-repo-card-lang-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }

/* The defensive/offensive coverage panels' own header -- title text plus a
   collapse/expand toggle, state persisted to localStorage (see team.js's
   PANEL_COLLAPSE_KEY) so it survives leaving the team page entirely, not
   just re-rendering within it. */
/* Sized to match the roster card's own small corner buttons (see
   .team-slot-bench) -- the original 28px/1.1rem size (still used nowhere
   else now that the two coverage panels are one) read as noticeably
   bigger than every other small icon-button in the team page's own
   visual language. */
.team-panel-collapse-btn {
  flex-shrink: 0; width: 20px; height: 20px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 6px; background: var(--bg-panel-alt);
  color: var(--text-dim); font-size: 0.65rem; line-height: 1; cursor: pointer;
}
.team-panel-collapse-btn:hover { border-color: var(--accent); color: var(--text); }

/* Title + caption are grouped into their own column and centered as a unit
   against the level slider, rather than the old layout where only the
   title (h2) sat on the slider's row and the caption ran full-width below
   it -- that left the title sitting visibly above-center relative to the
   slider, with the caption doing nothing to balance it. Centering the
   slider column itself against this whole stack (via align-items: center
   on the parent) keeps the slider exactly where it was and brings the
   title/caption pair up to meet it instead. */
.stats-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.stats-panel-head-title { display: flex; flex-direction: column; gap: 2px; }
.stats-panel-head h2 { margin: 0; }
.level-slider-wrap { display: flex; align-items: center; gap: 6px; font-size: 0.8rem; color: var(--text-dim); }
.level-slider-wrap input[type="range"] { width: 110px; accent-color: var(--accent); }
/* Locked to the Team Builder's stage cap (see detail.js's stageLocked) --
   grayed out via reduced opacity + not-allowed cursor on top of the
   browser's own default disabled-range look, so it reads as deliberately
   fixed rather than just unstyled. */
.level-slider-wrap input[type="range"]:disabled { opacity: 0.45; cursor: not-allowed; }
.stats-caption { font-size: 0.68rem; color: var(--text-dim); }

/* Moveset panel header: the Edit Moves button (only rendered when viewing
   a current Team Builder party member -- see detail.js) sits pinned to the
   panel's own upper-right corner, independent of the h2 text -- NOT a flex
   row sharing the title's own baseline (that read as "part of the
   Moveset heading" rather than its own control). h2 is left in normal
   panel flow (.panel h2's own margin/size rules apply unchanged); the
   button is taken out of flow entirely and centered vertically against
   the title row's own height. */
.moveset-panel-head { position: relative; }
.edit-moves-btn {
  position: absolute; top: 50%; right: 0; transform: translateY(-50%);
  padding: 7px 12px; border-radius: 8px; border: 1px solid var(--border);
  /* --bg-panel-alt (not --bg-panel, the panel's own background -- which
     made this button nearly invisible, same color as what's behind it)
     -- same lighter "floats above the panel" tone the Load/Stage
     dropdowns already use. */
  background: var(--bg-panel-alt); color: var(--text); font-size: 0.8rem; cursor: pointer; white-space: nowrap;
  box-shadow: 0 1px 4px rgba(0,0,0,0.3);
}
.edit-moves-btn:hover { border-color: var(--accent); }
.edit-moves-btn-active { border-color: var(--accent); color: var(--accent); }

/* Edit Moves table: narrow leftmost column carrying the +/checkmark
   control, otherwise the same .moves-table look. vertical-align: middle --
   same reasoning as .moves-table td.col-level (a single short control, no
   multi-line content, so it sits centered against the row's real height,
   governed by the taller Move cell, not pinned to the top). */
.moves-table-edit td.col-edit, .moves-table-edit th.col-edit { width: 28px; padding-left: 8px; padding-right: 2px; vertical-align: middle; }
.move-edit-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 6px; border: 1px solid var(--border);
  background: var(--bg-panel); color: var(--text); font-size: 0.85rem; font-weight: 700;
  cursor: pointer; line-height: 1; padding: 0;
}
.move-edit-add::before { content: "+"; }
.move-edit-add:hover:not(.move-edit-disabled) { border-color: var(--accent); color: var(--accent); }
.move-edit-disabled { opacity: 0.35; cursor: not-allowed; }
/* Added state: green checkmark by default, swaps to a red X purely via
   CSS on hover -- pressing it while showing the X removes the move (see
   detail.js's data-move-toggle handler, which just toggles regardless of
   which glyph is currently showing). */
.move-edit-added { background: color-mix(in srgb, var(--tier-half) 18%, transparent); border-color: var(--tier-half); color: #7be3a8; }
.move-edit-added::before { content: "\2713"; }
.move-edit-added:hover { background: color-mix(in srgb, var(--tier-4x) 18%, transparent); border-color: var(--tier-4x); color: #ffc9c4; }
.move-edit-added:hover::before { content: "\2715"; }
/* Locked rows get an inert placeholder, not a button -- nothing to press. */
.move-edit-locked { border-style: dashed; opacity: 0.35; cursor: not-allowed; }

/* Wide enough that no stat label (including "Sp. Attack"/"Sp. Defense")
   ever wraps to a second line, which was breaking the row's vertical
   rhythm before. */
.stat-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 0.85rem; }
.stat-row .stat-label { width: 76px; flex-shrink: 0; white-space: nowrap; color: var(--text-dim); }
.stat-row .stat-value { width: 34px; flex-shrink: 0; text-align: right; font-variant-numeric: tabular-nums; }
/* Faint gradation ticks every 20% of the bar's width, purely a visual
   reading aid -- the tick lines sit behind stat-bar-fill (which is opaque)
   so they only ever show through the *unfilled* portion of the bar. Spelled
   out as four explicit stops (20/40/60/80) rather than a repeating-gradient
   -- a repeating pattern with a 20%-wide unit also lands a tick right on
   the 100% boundary, which then gets visually sliced by the track's own
   rounded-corner edge and read as a stray cut-off mark. */
.stat-bar-track {
  position: relative;
  flex: 1;
  height: 8px;
  background-color: var(--bg-panel-alt);
  background-image: linear-gradient(
    to right,
    transparent 0, transparent calc(20% - 1px), rgba(255,255,255,0.09) calc(20% - 1px), rgba(255,255,255,0.09) 20%, transparent 20%,
    transparent calc(40% - 1px), rgba(255,255,255,0.09) calc(40% - 1px), rgba(255,255,255,0.09) 40%, transparent 40%,
    transparent calc(60% - 1px), rgba(255,255,255,0.09) calc(60% - 1px), rgba(255,255,255,0.09) 60%, transparent 60%,
    transparent calc(80% - 1px), rgba(255,255,255,0.09) calc(80% - 1px), rgba(255,255,255,0.09) 80%, transparent 80%
  );
  border-radius: 4px;
  overflow: hidden;
}
.stat-bar-fill { height: 100%; background: var(--accent); position: relative; z-index: 1; transition: width 0.15s ease; }
.bst-row { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border); }
.bst-row .stat-label { font-weight: 700; color: var(--text); }
/* The 20%-interval gradation ticks are useful on the six regular stat bars
   (all sharing one 0-255 scale, so a tick reliably means the same thing
   everywhere) but not on this one -- it's a percentile, not a raw stat, so
   the same tick marks don't carry the same meaning here. Drops back to a
   flat, mark-free track just for this row. */
.bst-bar-track { background-image: none; }
/* Background color is set inline per-row now (see renderBstRow in
   detail.js) -- a parse-percentile-style tier color (grey/green/blue/
   purple/orange/pink/gold) instead of a flat gold fill, so the bar
   communicates a tier at a glance instead of just a length. */
.bst-bar-fill { transition: width 0.15s ease, background 0.15s ease; }

.weakness-group + .weakness-group { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border); }
.weakness-tier { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.weakness-tier .tier-label { width: 60px; flex-shrink: 0; font-size: 1.05rem; font-weight: 800; align-self: center; }
.weakness-tier .tier-types { flex: 1; display: flex; gap: 5px; flex-wrap: wrap; }

/* table-layout:fixed previously forced the Move column to swallow all the
   width the table's other, small, fixed-px columns didn't use -- and since
   the table stretches to the full (now-wide) right column, that left a
   large empty gap in the Move cell between the description text and the
   Type/Cat/Pow/Acc columns after it. Auto layout (the default) sizes each
   column to its own content instead, so those columns stay tight and the
   Move column only takes what its (width-capped) description actually
   needs. Separately, a blanket overflow-wrap:break-word on every cell was
   letting 3-digit Power values wrap mid-number inside their narrow column
   ("120" -> "12" / "0") -- removed in favor of explicit white-space:nowrap
   on just the numeric/short columns, where wrapping should never happen. */
.moves-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
.moves-table th, .moves-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
/* Level-up's Lv column holds a single short number with nothing else in
   the cell, so it should sit centered against the row's full height
   (governed by the taller Move cell next to it), not pinned to the top
   like that multi-line cell needs to be. */
.moves-table td.col-level { vertical-align: middle; }
.moves-table th { color: var(--text-dim); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; cursor: pointer; user-select: none; white-space: nowrap; }
.moves-table th:hover { color: var(--text); }
/* Same default/reversed color scheme as the dex sort buttons/headers, and
   -- unlike before -- the reversed direction now genuinely re-sorts the
   table instead of silently always sorting one way. The arrow glyph sits
   in its own fixed-width slot that's always present (see th-arrow below)
   so a column's rendered width doesn't change -- and shift every column
   after it sideways -- the moment it becomes the active sort. */
.moves-table th.active-default { color: #bdf5d6; }
.moves-table th.active-reversed { color: #ffc9c4; }
.moves-table .th-arrow { display: inline-block; width: 10px; text-align: center; font-size: 0.6rem; }
/* These three headers' own data cells are center-aligned (see col-category/
   col-power/col-accuracy td rules below) -- the headers need the same
   alignment, or the column reads as visually mismatched (values centered
   under a left-hugging label). Lvl/Move/Type stay left-aligned since their
   cells are too.
   NOTE: an earlier attempt set display:flex directly on these <th>
   elements to "force" the centering -- that was wrong. Setting display to
   something incompatible with table layout on a table-cell knocks it out
   of the table's column layout entirely, which is exactly why CAT/POW/ACC
   started stacking vertically instead of sitting side by side as columns.
   Reverted to plain text-align (CSS-correct, doesn't disturb table
   layout), plus min-width so these narrow columns always have some slack
   for the centering to actually show.
   Still wasn't quite centered even after that fix, though -- because
   text-align:center centers the label+arrow *as one inline group*, and
   the arrow's reserved slot (see .th-arrow, always present even when
   empty, so the column doesn't resize when a sort becomes active) sits to
   the right of the label as part of that group. That bulks out the right
   side of the centered unit, which visibly drags the actual label text
   left of true-center. Pulling the arrow out of the inline flow entirely
   (position:absolute, pinned to the cell's own right edge) for just these
   three columns fixes that: text-align:center now has only the label left
   to center, so it lands on the column's real center line, and the arrow
   still sits right next to it since these columns aren't wide. Left-
   aligned columns (Lvl/Move/Type) keep the arrow inline as before --
   pulling it out there would leave it stranded far from the label on
   Move's much wider column. */
.moves-table th.col-category, .moves-table th.col-power, .moves-table th.col-accuracy { text-align: center; position: relative; }
.moves-table th.col-category .th-arrow,
.moves-table th.col-power .th-arrow,
.moves-table th.col-accuracy .th-arrow {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
}
.moves-table td.col-level { white-space: nowrap; }
.moves-table td.col-type { white-space: nowrap; }
.moves-table th.col-category, .moves-table td.col-category { min-width: 48px; }
.moves-table th.col-power, .moves-table td.col-power, .moves-table th.col-accuracy, .moves-table td.col-accuracy { min-width: 44px; }
.moves-table td.col-category { text-align: center; padding-left: 4px; padding-right: 4px; white-space: nowrap; }
.moves-table td.col-power, .moves-table td.col-accuracy { text-align: center; font-variant-numeric: tabular-nums; white-space: nowrap; }
.move-name-text { cursor: pointer; font-weight: 700; }
.move-name-text:hover { text-decoration: underline; filter: brightness(1.25); }
/* Narrower than before -- reclaiming that width for the left column,
   which had more room to give than the moveset table's description text
   actually needed. */
.move-desc { font-size: 0.72rem; color: var(--text-dim); margin-top: 2px; font-weight: 400; max-width: 340px; }
/* Same red used everywhere else banned/4x-weak/hardcore-mode content is
   flagged (--tier-4x) -- a low-alpha wash across the whole row, not just
   the struck-through name, so a banned move reads as banned at a glance
   while scanning the table rather than only once you notice the name text
   itself looks different. */
tr.banned { background: color-mix(in srgb, var(--tier-4x) 16%, transparent); }
tr.banned .move-name-text { text-decoration: line-through; opacity: 0.55; }
/* LOCKED (a move above this party's current Team Builder stage cap) --
   same red wash as BANNED above (still means "not usable right now"), but
   deliberately no strikethrough: unlike a mode-banned move, this one just
   hasn't been reached yet, not permanently off-limits, so the name itself
   stays intact and only the tag/tint communicate it. */
tr.locked { background: color-mix(in srgb, var(--tier-4x) 16%, transparent); }
.locked-tag { color: var(--tier-4x); font-size: 0.68rem; font-weight: 700; margin-left: 6px; text-transform: uppercase; letter-spacing: 0.03em; }
/* Edit Moves' own move-name span -- same look as .move-name-text (bold,
   type-colored) but deliberately NOT clickable/underline-on-hover, since
   Edit Moves rows don't wire the dex-filter click .move-name-text normally
   gets (data-move-id is only ever set in the read-only tables). Sharing
   .move-name-text's cursor/hover here would read as clickable and do
   nothing. */
.move-name-text-static { font-weight: 700; }
.move-from-tag { font-size: 0.72rem; color: var(--text-dim); font-weight: 400; }
/* Category icon PNGs (img/categories/*.png) rendered at full size, scaled
   down proportionally to fit the table row -- these were previously
   treated as a 2-frame sprite sheet and only half the image was shown. */
.cat-icon {
  display: inline-block;
  width: 40px;
  height: 16px;
  image-rendering: pixelated;
  vertical-align: middle;
}

/* Section heads ("Level-up (26)" etc.) styled as real table heads instead
   of plain dim text. */
.panel h3.section-head {
  margin: 18px 0 8px;
  padding: 6px 10px;
  background: var(--bg-panel-alt);
  border-left: 3px solid var(--accent);
  border-radius: 4px;
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text);
  font-weight: 700;
}
.panel h3.section-head:first-child { margin-top: 0; }

/* ---- evolution tree (horizontal) ----
   Each stage sits directly left of its own evolution(s); multiple
   evolutions from one stage stack as separate rows branching off that
   stage, not off each other -- e.g. Mega X and Mega Y both branch from
   Charizard independently, side by side, rather than one implying it
   evolves from the other. */
/* Padded on all sides (not just the bottom) so the current-node highlight
   has room to render fully even on nodes flush against the scrollable
   container's edge -- it was getting visibly cropped there before. */
.evo-chain-horizontal { display: flex; overflow-x: auto; padding: 8px; }
.evo-stage { display: flex; align-items: center; gap: 6px; }
.evo-branch-col { display: flex; flex-direction: column; gap: 8px; margin-left: 6px; }
/* Eevee-scale fan-outs (5+ siblings off one stage -- see detail.js's
   EVO_GRID_BRANCH_THRESHOLD) switch to a 2-column grid instead of one long
   vertical stack, which was the specific "hard to read as a long list"
   complaint. Each column still reads top-to-bottom as its own short list;
   the grid just lets a second column start once the first would otherwise
   run past 4 rows, roughly halving the stage's total height. */
.evo-branch-col.evo-branch-grid {
  display: grid; grid-template-columns: repeat(2, auto); grid-auto-flow: column;
  grid-template-rows: repeat(4, auto); gap: 8px 14px;
}
.evo-branch-row { display: flex; align-items: center; gap: 6px; }
/* The method text used to live here, under the arrow, which needed a wide
   fixed box to hold it and kept stages from sitting close together. It now
   lives inside the evo-node itself (above the sprite, see .evo-method
   below) so the arrow only ever needs to be as wide as the glyph. */
.evo-arrow-h { color: var(--text-dim); font-size: 1.05rem; flex-shrink: 0; }
/* Fixed width regardless of label length -- previously sized purely from
   content, so a short name like "Gallade" produced a narrower, single-line
   card while a longer one like "Wormadam-Sandy" wrapped to two lines and
   widened slightly, leaving rows of sibling cards visibly uneven (flagged
   against Gardevoir/Gallade's branch specifically). 94px is the 78px inner
   content width (sprite/name/method's own max-width) plus this card's own
   8px padding on each side. */
.evo-node { text-align: center; background: var(--bg-panel-alt); border-radius: 8px; padding: 8px; cursor: pointer; flex-shrink: 0; width: 94px; }
/* box-shadow instead of outline: outline doesn't participate in the box
   model at all (nothing reserves space for it), so it was getting sliced
   off by the scroll container's own edge; a box-shadow is painted inside
   normal paint bounds and respects the padding added above. */
.evo-node.current { box-shadow: 0 0 0 2px var(--accent); }
/* Reserves its line even when empty (the root/base stage has no incoming
   method) so every node's sprite still lines up at the same height. */
.evo-node .evo-method { font-size: 0.6rem; line-height: 1.15; min-height: 1.15em; max-width: 78px; margin: 0 auto 3px; color: var(--text-dim); }
/* Evolution-item labels (Fire Stone, Venusaurite, etc.) link out to the
   Special Items page (see detail.js's evoMethodHtml/itemLinkSpan) -- same
   hover-underline + brighten treatment as .ability-entry-name's own link
   styling elsewhere on this page, so both read as the same kind of
   clickable text. */
.evo-method-item { cursor: pointer; }
.evo-method-item:hover { text-decoration: underline; filter: brightness(1.3); }
/* Tinted by the Pokemon's own primary type, same treatment as the Pokedex
   Matrix card's own background -- previously just a flat panel-alt square
   like the rest of the card. */
.evo-sprite-box { width: 64px; height: 64px; margin: 0 auto; border-radius: 6px; overflow: hidden; }
.evo-node img { width: 64px; height: 64px; image-rendering: pixelated; display: block; }
/* min-height reserves 2 full lines (line-height 1.15 x 2, plus a hair of
   slack) whether or not a given name actually wraps -- single-line names
   ("Gallade") used to leave their card shorter than a wrapping neighbor's
   ("Wormadam-Sandy"), which is exactly what made same-row cards read as
   inconsistent. Centered within that reserved space rather than left
   sitting at the top, so a short single-line name doesn't look stranded. */
.evo-node .evo-name {
  font-size: 0.76rem; line-height: 1.15; margin: 2px auto 0; max-width: 78px;
  min-height: 2.5em; display: flex; align-items: center; justify-content: center;
}

.location-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
.location-table th, .location-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--border); }
.location-table th { color: var(--text-dim); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; }
.no-data { color: var(--text-dim); font-size: 0.85rem; font-style: italic; }

