/* ============================================================
   AceBase homepage — uncrate.com's magazine layout.

   Measured off uncrate at a 1440 viewport and reproduced here:

     content column   960px, centred
     two-column grid  48.9583% + 2.08333% gutter  ==  repeat(2,1fr) + 20px gap
     card             border 1px #c7c7c7, centred copy, image 3:2
     hero             full 960 width, image 3:2 (960x641), copy centred
     category label   12px bold uppercase, tan with a 1px matching underline
     card title       28px, uppercase, letter-spacing .75px
     hero title       32px; card excerpt 17px/25px, hero excerpt 18px/28px
     palette          text #000, border #c7c7c7, accent #ae9e85, muted #666

   uncrate sets headings in Knockout 49 and body copy in Sentinel, both
   Cloud.typography licences. The stand-ins are a 900-weight system sans for
   headings and a serif stack for excerpts, which is what gives the page its
   editorial read. CJK is not condensed, so Chinese headlines run wider than
   uncrate's — tracking and weight are the only levers available.

   Markup comes from scripts/gen_home.py.
   ============================================================ */

.ab-mag {
  --ab-sans: "PingFang SC", "Microsoft YaHei", Inter, -apple-system, "Segoe UI", sans-serif;
  --ab-serif: Georgia, "Songti SC", "Times New Roman", SimSun, serif;
  box-sizing: border-box;
  max-width: 960px;
  margin: 0 auto;
  font-family: var(--ab-sans);
  color: var(--ab-text);
}

.ab-mag *,
.ab-mag *::before,
.ab-mag *::after {
  box-sizing: border-box;
}

/* ---------- category label ---------- */
.md-typeset .ab-mag .ab-cat {
  display: inline-block;
  /* The card copy is a flex column, which stretches its children; without
     this the category's underline runs the full width of the card instead of
     hugging the label the way uncrate's inline-block does. */
  align-self: center;
  margin: 16px 0 17px;
  padding: 0 0 3px;
  border-bottom: 1px solid var(--ab-accent);
  font-size: 12px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  /* The decorative tan carries no text — the label itself is the deep
     variant of the same hue, which clears 4.5:1 on white. */
  color: var(--ab-accent-deep);
}

.md-typeset .ab-mag .ab-cat a {
  color: inherit !important;
  text-decoration: none !important;
}

/* ---------- date stamp ----------
   uncrate runs "Updated on <date>" up the left edge. Rotated so it reads
   bottom-to-top, and hung in the page margin so it costs the 960px column
   no width; below ~1100px viewport that margin no longer exists, so it goes.

   --ab-text-faint is uncrate's own #989898, but that is 2.88:1 on white and
   this is 11px text, so the stamps take --ab-text-muted (#666, 5.7:1) and
   keep their quietness from the size and the rotation instead. */
.ab-mag .ab-stamp {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.5px;
  white-space: nowrap;
  color: var(--ab-text-muted);
}

/* ---------- hero ---------- */
.md-typeset .ab-mag .ab-hero {
  margin: 0 0 48px;
  border: 1px solid var(--ab-line);
  text-align: center;
}

.ab-hero__media {
  position: relative;
}

.md-typeset .ab-mag .ab-hero__media img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  margin: 0;
  border-bottom: 1px solid var(--ab-line);
}

.md-typeset .ab-mag .ab-hero__copy {
  padding: 0 20px 20px;
}

.md-typeset .ab-mag .ab-hero__title {
  margin: 0 0 14px;
  padding: 0;
  border: 0;
  font-size: 32px;
  font-weight: 900;
  line-height: 1.15;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ab-text);
}

.md-typeset .ab-mag .ab-hero__title a {
  color: inherit !important;
  text-decoration: none !important;
}

.md-typeset .ab-mag .ab-hero__title a:hover {
  color: var(--ab-accent-deep) !important;
}

.md-typeset .ab-mag .ab-hero__excerpt {
  max-width: 44rem;
  margin: 0 auto 16px;
  font-family: var(--ab-serif);
  font-size: 18px;
  line-height: 28px;
  color: var(--ab-text-muted);
}

/* Article variant of the hero. The base .ab-hero is the homepage's centred
   teaser; an article page reads like uncrate's — left-aligned copy under a
   band of artwork. The band is height-capped because the game art is a square
   800×800 cover: left at the base 4:3 it would stand 720px tall and push the
   prices off the first screen. object-fit stays `contain` so no part of the
   artwork is cropped, and the frame behind it reads as a deliberate plate. */
.md-typeset .ab-mag .ab-hero--article {
  text-align: left;
}

.md-typeset .ab-mag .ab-hero--article .ab-hero__media img {
  aspect-ratio: auto;
  height: 400px;
  object-fit: contain;
  background: var(--ab-panel-2);
}

/* A band holding a 12:5 product photo takes its height from the photo itself.
   The fixed 400px above is sized for square artwork, which only overflows that
   box on a phone: a 2.4:1 photo in a 356px-wide, 400px-tall plate would be a
   148px strip over 250px of empty plate. Letting the ratio size the band keeps
   the desktop band at its 400px and collapses it on narrow screens. */
.md-typeset .ab-mag .ab-hero--photo .ab-hero__media img {
  height: auto;
}

/* `--brand` lived here: an `ab-hero` band holding a brand mark on a fixed
   200px plate, for the fourteen 24:5 marks on /games/live-prices/. That page
   went on 2026-09-21 and the marks moved onto /topup/ as ordinary cards, where
   the 24:5 frame in the card's media box does the same letterboxing job. The
   other two treatments for that plate are still in use: `--photo` above and
   `--article` below. */

.md-typeset .ab-mag .ab-hero--article .ab-hero__title {
  font-size: 30px;
}

.md-typeset .ab-mag .ab-hero--article .ab-hero__excerpt {
  max-width: none;
  margin: 0 0 18px;
}

/* ---------- folded band copy ----------
   The GPU bands keep their lead sentence visible and put the price rows and the
   fetch date behind a click, the way an uncrate listing keeps one line out and
   the rest behind Read More. It is a <details>, not a script: the summary is a
   real focusable control, so the fold opens from the keyboard and survives
   JavaScript being off — on a page whose whole point is the figures, a fold
   that needed JS to open would hide them from anyone whose JS failed.

   Material dresses <details> as an admonition: a blue rule, a tinted title bar,
   a mask icon on the title's ::before and a chevron on its ::after. The first
   two rules take all of that off before anything below can lay out. The
   background needs the !important because extra.css paints every summary and
   every details in the theme's admonition colours with !important of its own —
   the fold is not an admonition, so it answers at the same weight. */
.md-typeset .ab-mag .ab-fold,
.md-typeset .ab-mag .ab-fold__summary {
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none !important;
  box-shadow: none;
}

.md-typeset .ab-mag .ab-fold {
  font-size: inherit;
}

.md-typeset .ab-mag .ab-fold__summary::before,
.md-typeset .ab-mag .ab-fold__summary::after {
  display: none;
}

/* The lead is set exactly like the .ab-hero__excerpt it replaces, so a band
   reads the same whether or not it has been opened. */
.md-typeset .ab-mag .ab-fold__summary {
  display: block;
  max-width: 44rem;
  margin: 0 0 18px;
  font-family: var(--ab-serif);
  font-size: 18px;
  font-weight: inherit;
  line-height: 28px;
  color: var(--ab-text-muted);
  cursor: pointer;
  list-style: none;
}

.md-typeset .ab-mag .ab-hero--article .ab-fold__summary {
  max-width: none;
}

.md-typeset .ab-mag .ab-fold__summary::-webkit-details-marker {
  display: none;
}

.md-typeset .ab-mag .ab-fold__summary:hover .ab-fold__lead {
  color: var(--ab-text);
}

/* The cue borrows the .ab-hero__more link treatment — same size, same tan rule
   under it — so it reads as the same kind of affordance as the links beside
   it. */
.md-typeset .ab-mag .ab-fold__cue {
  margin-left: 6px;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--ab-accent);
  font-family: var(--ab-sans);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.5px;
  white-space: nowrap;
  color: var(--ab-accent-deep);
}

.md-typeset .ab-mag .ab-fold__summary:hover .ab-fold__cue {
  border-bottom-color: var(--ab-text);
  color: var(--ab-text);
}

/* One cue at a time. Both live in the markup rather than in ::after content so
   the words stay in the document, where a translator or a screen reader can
   reach them. */
.md-typeset .ab-mag .ab-fold__cue-less,
.md-typeset .ab-mag .ab-fold[open] .ab-fold__cue-more {
  display: none;
}

.md-typeset .ab-mag .ab-fold[open] .ab-fold__cue-less {
  display: inline;
}

/* ---------- the same fold inside a card ----------
   /tech lists the nine models as cards rather than as full-width bands, so the
   fold turns up in a column roughly a third the width. Everything above is
   measured for a band and has to come down a size: the lead matches
   .ab-card__excerpt (the line it stands in for), the cue leaves the run-on of
   the lead for an action line of its own, and the rows tighten, because
   "历史最低（2025-12-25）" and a price at 18px with 1.9 leading wrap inside
   270px, and four rows of that reads as a wall.

   These rows are also the one part of the card that does not take .ab-card's
   centring. The rest of the copy is a sentence and reads as one; this is a
   short table, and a table is read down its column — centred, the figure being
   compared sits somewhere new on every row. So the label takes the left edge,
   the figures share a column of their own (the grid, below), and everything
   above and below them stays centred.

   First, one thing has to be got out of the way. extra.css carries a legacy
   `.md-typeset .ab-card span` rule — 0.82rem / 1.45 — from the card template
   these replaced, and it matches every span inside every card, these included.
   A directly-matching rule beats inheritance, so it was quietly setting the
   size of type this block thought it owned: the price rows rendered at 16.4px
   where the card asks for 15, the lead at 16.4px where the summary asks for 17,
   and the cue's own inner spans at 16.4px grey — the one word on the card that
   is supposed to read as a link, rendering as neither the size nor the colour
   its rule gives it. Hand these spans back to inheritance and let the rules
   below decide; the cue rule wins over this one on specificity, so the cue
   itself keeps its own size while the 展开/收起 inside it follows.

   The four are named one by one rather than as `.ab-fold span`: that selector
   counts four classes and would outrank the cue's own rule below, pulling the
   cue up to the summary's 17px. .ab-fold__act is in the list for a second
   reason — it is a span itself, so it was passing 1.45 down to the <em> and the
   <a> inside it, which no rule of theirs could reach. */
.md-typeset .ab-mag .ab-card .ab-fold__lead,
.md-typeset .ab-mag .ab-card .ab-fold__act,
.md-typeset .ab-mag .ab-card .ab-ec-list span,
.md-typeset .ab-mag .ab-card .ab-fold__cue span {
  font-size: inherit;
  line-height: inherit;
}

/* Sizes only, above: colour stays alone because .ab-ec-price sits inside the
   fold too and its green is not something to inherit away. The cue's own words
   do need it back — the legacy rule was painting the one word on the card that
   is meant to read as a link in muted grey. */
.md-typeset .ab-mag .ab-card .ab-fold__cue span {
  color: inherit;
}

.md-typeset .ab-mag .ab-card .ab-fold__summary {
  margin-bottom: 0;
  font-size: 17px;
  line-height: 25px;
}

.md-typeset .ab-mag .ab-card .ab-ec-list {
  margin: 14px 0 0;
  font-size: 15px;
  line-height: 1.75;
}

.md-typeset .ab-mag .ab-card .ab-ec-foot {
  margin: 12px 0 0;
  font-size: 11px;
}

/* ---------- the price block, readable with the fold shut ----------
   The four rows are the reason the card exists, and shut they were the one
   thing it did not show: a photo, a model name, a sentence, and what anything
   costs was behind the fold. Two things are owed to a reader who has not
   opened it — the price, and a sign that there is more.

   A shut <details> hides its content whole; there is no partially open one. So
   the rows cannot stay inside the fold and be half readable, and they sit
   beside it instead: the fold's own `open` attribute reaches them through the
   sibling combinator, `.ab-fold[open] ~ .ab-ec-list`. No :has() and nothing
   that needs JavaScript — the fold is a state holder now, owning `open` and the
   cue that flips with it, and the block it "reveals" is the next sibling.

       shut   当前市场均价         $419.    <- red, 21px
              历史最低（2025-12-25） $254.
              近 30 天均价         $406.    <- the cut, under the fade
              展开 或 去亚马逊购买

       open   all four rows, then the footnote, then 收起 或 去亚马逊购买

   The rows are a two column grid, and the label column is max-content, so the
   figures all start on the same line however wide the label is. That is what
   the slash used to do — it separated the two runs, and without it they would
   be one run of text with the value starting at a different x on every row.
   The full stop came inside the price when the slash left: left outside it, it
   would be a third item in the grid and would start a column of its own. The
   <br>s between rows are hidden for the same reason — a break has no business
   in a grid cell, and measured, each one takes a 26.25px row of its own and
   sends the next label into the figure column.

   The type is --ab-sans where the ladder on the article pages is --ab-serif,
   and the figures take a Latin-first stack on top of it: --ab-sans leads with
   the CJK faces, which is right for a sentence and heavy for a column of
   digits. Inter or Segoe UI carry the cleaner numerals, with tabular figures
   so the column is aligned digit for digit and not just at the left edge.

   The cut is the sign, so it has to land mid-row rather than between two. The
   block is 36.75px for its first row and 26.25px for each of the other three:
   the first is taller because its price is 21px and .ab-ec-list states
   line-height as a ratio, which resolves against each element's own size. 84px
   holds the first two rows whole and most of the third, and the mask fades the
   last 22px so the cut reads as an edge and not as a fault.

   Not a guessed number: every one of the nine cards was measured at 1440, 1100,
   900, 767, 600, 480, 390, 375, 360, 340 and 320px, and the cut was checked
   against the third row's own box at each of them — above the block's bottom
   edge and ending below it, everywhere. What would break it is a wrapped row
   above the cut, so that is what the measurement looked for — and the two rows
   that come close are not the same row. The dated lowest is a long label with
   a short price (230px in the grid, 226px as a line) and the four-figure range
   is a short label with a long price (306px in the grid, 201px as a line), so
   of the four rows the range is the only one that ever strains, and it is the
   one below the cut.

   The red is --ab-red, which is what .ab-card__meta .ab-money already paints a
   price in, so the number reads as a price here the way it does there.

   Two classes, not one, because the rows and the fold are two things. --ec is
   the block: the grid, the shared figure column, the type. --fold is what the
   card does with it: clip it, fade it, hide the footnote behind it, put the
   action line in the space it reserves. Every 显卡 card on /tech carries both.
   The Osmo Pocket 3 card carries --ec alone — its block is two rows, no taller
   than it looks, so there is nothing to clip and nothing for a 展开 cue to
   open; the same cap of 84px would never be reached and the 22px fade would
   land across the second row, greying a row it is not hiding. */
.md-typeset .ab-mag .ab-card--ec .ab-ec-list {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: 1.1em;
  text-align: left;
  font-family: var(--ab-sans);
}

/* The cut, and the reason the block is clipped at all: 84px holds two rows and
   most of a third, so a shut card shows what the model costs and says there is
   more by cutting one row in half rather than ending on a clean edge. */
.md-typeset .ab-mag .ab-card--fold .ab-ec-list {
  max-height: 84px;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
}

/* A maximum is the right shape for a ladder that runs past the window, and the
   wrong one for a card whose ladder is shorter than it. Nintendo Switch Online
   sells two tiers — 64px of rows — so that card came out 20px short of the
   PlayStation card beside it on every two-column width, the last ragged row on
   /topup once the titles and the sentences were pinned. Fixed for --shop alone:
   the five game cards all run past 84px and must keep clipping to it, and
   --goods above turns the window off entirely.

   With the height pinned the mask fades its last 22px over empty space and
   paints nothing, which is the same no-op it is on a four-line sentence whose
   box the fold never dims. */
.md-typeset .ab-mag .ab-card--shop .ab-ec-list {
  height: 84px;
}

.md-typeset .ab-mag .ab-card--shop .ab-fold[open] ~ .ab-ec-list {
  height: auto;
}

.md-typeset .ab-mag .ab-card--ec .ab-ec-list br {
  display: none;
}

/* contents, so the label and the price are what the grid places rather than
   the row around them — the row keeps its place in the markup and gives up its
   box. */
.md-typeset .ab-mag .ab-card--ec .ab-ec-item {
  display: contents;
}

.md-typeset .ab-mag .ab-card--ec .ab-ec-label {
  color: var(--ab-text-muted);
}

.md-typeset .ab-mag .ab-card--ec .ab-ec-price {
  font-family: Inter, "Segoe UI", "Helvetica Neue", Arial, var(--ab-sans);
  font-variant-numeric: tabular-nums;
}

/* Under 390px the grid has to go, and the threshold is not the one the first
   measurement suggested. In the grid every price starts where the label column
   ends — 174px, set by "历史最低（2025-12-25）" — so a four-figure range like
   "$2,019 – $3,975." needs 306px of block to sit on one line, whatever its own
   label is. A 390px viewport leaves 316px and a 375px one leaves 301, and at
   301 the price wraps inside its column. The card measured first was the RTX
   5050, whose ranges are three figures, and it holds at 320; four of the nine
   have the longer ones.

   As a line rather than a grid the same content needs far less room, because
   the price no longer starts after the widest label — only after its own. The
   widest row becomes the dated lowest with its price, 226px against the 246px a
   320px viewport leaves, and the four-figure row is 201px. So the value keeps
   its column, read from the other end: the label takes the left edge and the
   auto margin pushes the price to the right one. Auto margin rather than
   justify-content because it is the same rule either way — one line or the
   second one, if a longer date or a five-figure price ever puts the price on a
   line of its own, it still lands against the right edge instead of the left. */
@media screen and (max-width: 389px) {
  .md-typeset .ab-mag .ab-card--ec .ab-ec-list {
    display: block;
  }

  .md-typeset .ab-mag .ab-card--ec .ab-ec-item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.75em;
  }

  .md-typeset .ab-mag .ab-card--ec .ab-ec-price {
    margin-left: auto;
    text-align: right;
  }
}

.md-typeset .ab-mag .ab-card--fold .ab-fold[open] ~ .ab-ec-list {
  max-height: none;
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
}

.md-typeset .ab-mag .ab-card--ec .ab-ec-item:first-child .ab-ec-price {
  color: var(--ab-red);
  font-size: 21px;
}

/* The footnote dates the block it sits under, so it belongs with the four rows
   rather than with the one line the shut card shows. */
.md-typeset .ab-mag .ab-card--fold .ab-ec-foot {
  display: none;
}

.md-typeset .ab-mag .ab-card--fold .ab-fold[open] ~ .ab-ec-foot {
  display: block;
}

/* The action line itself lives further down, beside .ab-card__more: it shares
   its leaf treatment with the homepage's CTA and has to be written after that
   rule to win on source order. */

/* ---------- the action line, at the bottom of the card ----------
   A <details> always renders its <summary> before the content it reveals, so
   once the card had rows below the lead, the control came out between them:

       Blackwell 架构、8GB GDDR7 的中端显卡，现价 $419，近 30 天上涨 6.3%。
       收起 或 去亚马逊购买                      <- the row, in the middle
       当前市场均价 / $419.
       …

   The summary cannot follow the rows in source order, and reordering it is not
   available either: `display: flex` on <details> plus `order` on the summary
   does nothing, because Chrome renders a <details> through a shadow slot — the
   summary is not a flex item of it, and the closed state stops hiding the rows.
   Measured, not assumed.

   So the row leaves the flow instead. The copy column becomes the containing
   block and reserves the row's height as bottom padding; the row is pinned into
   that padding, which puts it under whatever the card ends with — the footnote
   when open, the cut price block when shut.

   The summary has to go back to `position: static` first, or it intercepts:
   the computed style says a summary is `position: relative` — no rule naming it
   turned up when the stylesheets were walked, so it is the theme's, set from
   somewhere this file cannot see — and an absolutely positioned box resolves
   against the nearest positioned ancestor. Left alone, `bottom: 0` measured to
   the summary's own bottom edge and the row sat at 933-961px shut and 932-960px
   open, the same middle it started in. Static, it lands at the copy column's
   bottom: 979-1007 shut and 1127-1155 open, below the footnote at 1109.

   The reservation is a flat 46px for a 28px row. Measured at 1440, 1100, 900,
   767, 600, 480, 390, 360 and 320px the row's own width is 151-160px against
   246-693px of space, so it has at least 86px of slack and does not wrap at any
   of them. A wrapped row would be taller than the padding and would land on the
   block above it, so that measurement is the thing this number rests on.

   The two 数码好物 cards drop it to the row's own 28px, further down beside the
   rest of their rules. The 18px that gives up was slack on those two cards and
   nothing else: their footnote is revealed by the fold but sits in flow *above*
   this padding, so the only thing the 18px ever held was the gap between the
   sentence and the row — which is exactly the room the fade now needs.

   `.ab-card--fold` scopes the two rules below. They hang off .ab-card__copy,
   which every card on the site has, and without a hook only these nine carry
   the homepage's forty-one cards would each reserve 46px for a row they never
   draw. The descendants above do not need it: .ab-fold__lead and the rest exist
   nowhere but here.

   `margin-top: auto` is what pins the fold — and with it everything after it —
   to the bottom of the copy column. That used to fall to .ab-card__more's own
   `margin: auto 0 0`, and dropping it would leave the line floating wherever
   the prose happened to end on a card the grid has stretched. */
.md-typeset .ab-mag .ab-card--fold .ab-card__copy {
  position: relative;
  padding-bottom: 46px;
}

.md-typeset .ab-mag .ab-card--fold .ab-fold {
  margin-top: auto;
}

/* ---------- what folds when the price block is too short to ----------
   The two 数码好物 cards carry a two row price block, below the 84px cap and now
   with no window around it at all, so the block can never be what gets cut. What
   can still overrun the card is the sentence — the Action 5 Pro's spec line
   wraps to three lines where every other card's takes two, which was 25px of
   extra card and a pair of cards that did not match each other.

   Both cards put that sentence *below* the block — 标题 → 价格 → 句子, where
   the 显卡 cards read 句子 → 价格. Nothing here cares which side it is on: the
   clamp is on the sentence, and the block's own `margin: 14px 0 0` and the
   sentence's 25px lines simply swap places, which is why both cards still
   measure 652px against the 显卡 cards' copy column.

   One consequence of the swap is worth naming, because it is the reason the
   block is asserted uncut. A block below the fold is un-clipped on open by
   `.ab-fold[open] ~ .ab-ec-list`, a sibling combinator that only reaches
   forward; a block *above* the fold cannot be reached by it at all. These two
   blocks are two rows with no cap left on them, so the rule being dead costs
   nothing — but it is dead on these two cards, and a third row would be cut
   with nothing able to open it. (The rule that would have done it is the one
   the --goods block above switches off, so this is now a property of the CSS
   rather than a coincidence of row count.)

   So on these two the fold moves up a level and holds the sentence instead —
   and the sentence is cut the way a 显卡 card's block is cut: two lines shown,
   the third sliced by the same fade, the rest behind the 展开 cue. The card
   stays on the same 287px copy column as its neighbours.

   A fade rather than the ellipsis this used to wear (`-webkit-line-clamp: 2`),
   because the fade is the cue this page already teaches. The 显卡 cards say
   "there is more" by cutting a row in half, and an ellipsis says it a different
   way in the one place a reader has just learned the first.

   The 18px of fade does not come out of the card, and it does not come out of
   the sentence's own two lines either — both would have shown. It comes out of
   two pockets of slack that were already on the copy column and were already
   painting nothing:

     * the price block's 84px window. Two rows are 63px, and `align-content:
       start` packs them at the top, so the bottom 21px was empty and the block's
       own fade painted over that emptiness. Here the window is dropped to the
       63px it actually holds and the same 21px becomes a margin above the
       sentence — the sentence does not move, because 63 + 21 is the 84 the
       block used to occupy.
     * the 46px the copy column reserves for the action row. The row is 28px and
       is pinned to the bottom, so the 18px above it was empty too. The reserve
       drops to those 28px and the sentence's box grows by the 18.

   Adding both up: the sentence's box is 50px of two lines plus 18 of fade, and
   the action row is where it always was. Every element on the card measures
   what it measured before — which is the only reason the two cards still come
   to 652px with the 显卡 cards beside them.

   The row is 28px tall and now has exactly 28px reserved, so it must not wrap;
   it never has at any width, and the assertion that it still does not is in
   verify_osmo.py rather than in this comment.

   A sentence that fits in two lines is not dimmed by any of this: the fade
   starts exactly at the second line's bottom edge, so for Pocket 3 it paints
   nothing, the same trick the block plays one element above it. */
.md-typeset .ab-mag .ab-card--goods .ab-ec-list {
  max-height: none;
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
}

.md-typeset .ab-mag .ab-card--goods .ab-fold {
  margin-top: 21px;
}

.md-typeset .ab-mag .ab-card--goods .ab-card__copy {
  padding-bottom: 28px;
}

.md-typeset .ab-mag .ab-card--goods .ab-fold__lead {
  display: block;
  height: 68px;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
}

.md-typeset .ab-mag .ab-card--goods .ab-fold[open] .ab-fold__lead {
  height: auto;
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
}

/* ---------- the one line height these two cards have to agree on ----------
   Every rule above decides where the sentence is cut. None of them decides
   how tall the title is, and the title is the only thing on these two cards
   whose height depends on how its text happens to wrap.

   标题 → 价格 → 句子 is the order they read, so the title sits above the
   fold — above every rule that could have absorbed a line. "DJI Osmo Action
   5 Pro" is the longer of the two names, and measured on this page it takes
   two lines at two separate column widths: below 343px while the title is
   26px, and below 296px while it is the 22px that 767px switches it to.
   Those two widths land on two separate viewport bands — 768–836px, where
   the grid is still two columns, and 372px and under, where it is one — and
   nothing wraps in the 372–767px between them. On each band the two cards
   fall 30px and 25px apart, on a grid that keeps every other pair level.

   Reserving two lines for every --goods title takes the wrap out of the
   card's height. The query is a container, not a viewport, because the
   column is what decides the wrap: a viewport breakpoint would have to name
   both bands and would go stale the moment either name changed length,
   which is exactly the brittleness this avoids. 360px is the column width
   at which even the 26px title still fits on one line — at 1440px the
   column measures 417px, so the desktop card keeps the 652px its 显卡
   neighbour has, and the band below 372px is covered by the same number.
   The 360 leaves a few px of slack over the 343 the 26px title actually
   needs, because the fallback font decides those last few and this site
   loads none. One band pays for that slack: at 840–870px the reservation
   is on where nothing would have wrapped, and the two cards come out 30px
   taller than the 显卡 card below them. That card shares no row with them
   — three cards in a two column grid leave it alone in the second — so
   the row that does hold a pair is still level, which is the whole point.

   `2lh` rather than a px figure: line-height is the number that decides how
   tall two lines are, and 2lh is that number read back — it stays right if
   the title's type ever moves. */
.ab-card--goods .ab-card__copy {
  container-type: inline-size;
}

@container (max-width: 360px) {
  .md-typeset .ab-mag .ab-card--goods .ab-card__title {
    min-height: 2lh;
  }
}

/* ---------- the title reservation every card in that grid needs -----------

   This block used to reserve two heights on the price cards: the sentence
   under the title, and the title itself. The sentence's half — a min-height on
   .ab-fold__lead, band by band, keyed on `--brand` because every card on
   /topup/ carried a blurb — came out on 2026-10-05. /topup/ was the last of
   the three price pages still folding a ladder open; it followed /cdkeys/
   (2026-10-04) and /gift-cards/ (2026-10-05) onto the spec picker and dropped
   its blurbs in the same edit, so no `--brand` card on the site carries an
   ab-fold__lead any more and there is no sentence left to reserve for. The
   other ab-fold__lead rules in the file are not this card shape's and are
   untouched; the one lead left on the site is the gpu-deals hero's.

   What stays is the title reservation below — the newer of the two, added when
   the long product names moved in beside the short ones. `.ab-card__copy`
   keeps its container so those rules can query the column. */
.ab-card--brand .ab-card__copy {
  container-type: inline-size;
}

/* ---------- and the reservation every card in that grid needs on top -------
   The paragraph above says the brand cards' only variable-height element was
   the sentence. That was true of five short game names and stopped being true
   the moment the long product names moved in beside them — "PlayStation
   Network 充值卡（美国）" and "Nintendo Switch Online 会员资格" take two lines
   in a column that fits "Honor of Kings" on one, three once the column is
   narrow. The two cards sharing a row then sit 30px apart on a grid that keeps
   every other pair level, so the title gets a reservation of its own. Long
   titles are not a gift-card thing either: "Xena Live：群组语音" is one of the
   live-platform names. The modifier names the card shape — title and sentence
   both able to run long — not the section it sits in.

   It hangs off `--brand`, which is every card on /topup/ and, since they came
   onto the picker, every card on /cdkeys/ and /gift-cards/ too; it is not off
   `--shop`, which is only twenty-seven of them. That is the one thing here
   that changed after it was written, and it is worth the paragraph. Until
   2026-09-22 /topup/ was three grids — five game cards, thirteen gift cards,
   fourteen live platforms — and no row ever held one shape beside the other,
   so a reservation tuned for one family could not disturb another and the
   narrower selector was the honest one. Merging the three sections into one
   grid put 燕云十六声 (plain `--brand`) in a row with Amazon 礼品卡（美国）
   (`--shop`), and the pair came out 30px apart: the shop card reserved a second
   title line and the brand card reserved none. The fix is not a third number
   for the mixed pair — it is to say once, on the selector both shapes share,
   what a card in this grid reserves. `--shop` still means what its name says
   further up the file, where the two shapes genuinely differ: the price ladder
   that pins a `--shop` fold to a flat 84px because its longest tier is short.

   The number is measured on /topup, in content boxes, across 768-1440px:
   three lines at 340px and below, two from 342px up. The query switches at 350
   — the 10px of slack is the same fallback-font allowance the paragraphs above
   take, and it is paid for on 816-836px of viewport, where a title that would
   have held two lines is given three.

   Every value here is a container query, not a viewport one, for the reason
   the --goods paragraph gives: the column decides the wrap, not the window.
   The one viewport condition is the grid's own — below 768px .ab-list is a
   single column, no card has a row partner, and none of this is asked for.
   Reusing that exact breakpoint rather than guessing a container width is
   what keeps the reservation off the one-column band, where it would only
   be air: at 500-767px the copy column is 426px and wider, comfortably
   inside the range these queries reserve for.

   The cost, paid knowingly: the five game cards, whose names never reach two
   lines at 1440px, each carry one reserved line they do not fill. A row that
   is level is worth more than a card that is tight — that is the rule at the
   top of this file, and it is asserted width by width in verify_topup_cards.py. */
@media screen and (min-width: 768px) {
  @container (max-width: 350px) {
    .md-typeset .ab-mag .ab-card--brand .ab-card__title {
      min-height: 3lh;
    }
  }

  @container (min-width: 351px) {
    .md-typeset .ab-mag .ab-card--brand .ab-card__title {
      min-height: 2lh;
    }
  }

  /* Three to a row puts the copy column at 265px — 273 between 1024 and 1180,
     where the grid is a few px wider — and that is under the 350 the two rules
     above answer for. So their figure is being asked in a column it was not
     measured in, and it comes out wrong there: the title wants two lines
     rather than the three the 350-and-below rule reserves. It is not close —
     at the 17px this band sets, even "PlayStation Network 充值卡（美国）"
     holds two lines at 265px, and no card on the three pages takes three.

     A band of its own rather than a bigger number on those rules, because the
     next copy column up is 324px (a 768px viewport, still two columns) and it
     wants three. Carrying that figure up to it would put a line of air on
     every card between 768 and 1023 to pay for three columns of viewport. 300
     sits in the 273..324 gap — 27px clear above, 24 below — which is the
     fallback-font allowance the paragraphs above take.

     The figure is zero-slack against today's wrapping, as the 351-and-up title
     rule already is: 324px is the narrowest column the three-line title case
     occurs in, and 265 is not near it.

     Keyed on `--brand` like the rules it corrects — it and `--plate` select
     the same cards, every one of them a price card on these three pages plus
     its copy in the homepage feed. */
  @container (max-width: 300px) {
    .md-typeset .ab-mag .ab-card--brand .ab-card__title {
      min-height: 2lh;
    }
  }
}


.md-typeset .ab-mag .ab-card--fold .ab-fold__summary {
  position: static;
}

/* The cards carry the ids — the jump lists that used to point at them are gone,
   but a card URL is still a link anyone can be sent — `/gear/#gear-13` since
   2026-10-06 (before that `/gear/#gear-01`, and `/tech/#desk-01` for the two
   days before that — those ids went when their cards did) — so they keep the
   offset off the top edge the bands had. */
.md-typeset .ab-mag .ab-card {
  scroll-margin-top: 4.5rem;
}

/* A grid whose cards open. The default grid stretches every card in a row to
   the tallest, which is what keeps a row of closed cards even — and what puts
   a few hundred pixels of nothing into the card beside an opened one. Measured
   both ways on /tech: closed, every card is 536px either way, so the stretch
   is not what makes that page tidy; the fixed media frame is. Opened, the
   stretch is the only thing that moves the neighbour (684px), so this page
   opts out of it. */
.ab-list--expandable {
  align-items: start;
}

/* The homepage feed, where the reason is sharper than on /tech. A category
   page holds one kind of card; the feed mixes text-only cards with three image
   ratios in the same grid, and the stretch is what decides how a row of them
   reads. Left on, a live-platform card — a title, a one-line note and a price,
   maybe 180px of content — is padded out to the height of the 3:4 gift-card
   portrait beside it, and because .ab-card__copy is `flex: 1` the padding lands
   *inside* the card as a hole between the note and the CTA. `start` sizes each
   card to its own content, so the row is ragged at the bottom instead of
   hollow in the middle. */
.ab-list--feed {
  align-items: start;
}

/* Article pages stack several of these under the model jump list. The site
   header is static, so nothing is covering the target — the offset is only
   there to keep a jumped-to band off the very top edge, matching the 4.5rem
   the summary cards used before. */
.md-typeset .ab-mag .ab-hero--article {
  scroll-margin-top: 4.5rem;
}

/* ---------- tier price list ----------
   uncrate's Everyday Carry treatment: one line per tier, "名称 / $价格." —
   a name, a slash, a price, nothing else. Used by article pages that lead
   with the headline price instead of the full lowest/highest/average table.
   The price takes --ab-green, the same colour the tables give their 最低
   column, so "cheapest" reads the same way across the site. */
.md-typeset .ab-mag .ab-ec-list {
  margin: 0 0 20px;
  font-family: var(--ab-serif);
  font-size: 18px;
  line-height: 1.9;
  color: var(--ab-text);
}

.md-typeset .ab-mag .ab-ec-sep {
  padding: 0 0.45em;
  color: var(--ab-text-muted);
}

.md-typeset .ab-mag .ab-ec-price {
  font-weight: 700;
  color: var(--ab-green);
}

.md-typeset .ab-mag .ab-ec-foot {
  margin: 0 0 18px;
  font-size: 12px;
  color: var(--ab-text-muted);
}

/* The tier ladder is fetched, so this line is what shows until it lands. It
   reserves two rows' worth of height so the copy below it does not jump when
   the ladder arrives — the tallest ladder on the price pages is 17 rows, so
   reserving all of it would be worse than the shift. */
.md-typeset .ab-mag .ab-ec-loading {
  display: inline-block;
  min-height: 3.8em;
  font-size: 15px;
  color: var(--ab-text-muted);
}

/* The saving against the source's own list price, carried on the entry tier —
   the ladder is quoted at the reference price, so without this the page's one
   discount signal would be invisible. Painted like .ab-card__off. */
.md-typeset .ab-mag .ab-ec-off {
  margin-left: 0.5em;
  padding: 0.08em 0.45em;
  border-radius: 999px;
  background: rgba(21, 128, 61, 0.10);
  font-family: var(--ab-sans);
  font-size: 13px;
  font-weight: 700;
  color: var(--ab-green);
}

/* ---------- sections ---------- */
.ab-section {
  position: relative;
  margin: 0 0 56px;
}

.ab-section__head {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin: 0 0 20px;
  padding: 0 0 10px;
  border-bottom: 1px solid var(--ab-text);
}

.ab-section > .ab-section__head > .ab-stamp {
  position: absolute;
  left: -42px;
  top: 3px;
}

.md-typeset .ab-mag .ab-section__title {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 24px;
  font-weight: 900;
  line-height: 1.2;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ab-text);
}

/* The dek under a section head. `ab-hero__excerpt` cannot be reused here: it
   is centred with an auto margin, which fights the left-aligned head it would
   sit beneath. */
.md-typeset .ab-mag .ab-section__lead {
  max-width: 52rem;
  margin: -6px 0 0;
  font-family: var(--ab-serif);
  font-size: 18px;
  line-height: 28px;
  color: var(--ab-text-muted);
}

/* ---------- card grid ---------- */
.ab-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
}

.md-typeset .ab-mag .ab-card {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0 0 20px;
  border: 1px solid var(--ab-line);
  background: var(--ab-panel);
  text-align: center;
}

/* ---------- media frames ----------

   uncrate's grid is a uniform 3:2 because uncrate shoots 3:2. Almost nothing
   here is: the GPU photos are 4:3, the game art 800x800, the gift-card art
   360x480 and the wallpapers around 1:2. Cover-cropping those into a 3:2 box
   took the ends off the wider ones and a third out of the rest, so each
   section gets the frame its own images are drawn at. The generator tags the
   card; the ratios live here so they stay in one place. */
.md-typeset .ab-mag .ab-card__media img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  margin: 0;
  border-bottom: 1px solid var(--ab-line);
}

/* The GPU product photos, drawn at 4:3 to match the box the charts used to
   fill — the card itself sits in a white field inside that frame. */
.md-typeset .ab-mag .ab-card--photo .ab-card__media img {
  aspect-ratio: 4 / 3;
}

/* 800x800 game illustrations. */
.md-typeset .ab-mag .ab-card--square .ab-card__media img {
  aspect-ratio: 1 / 1;
}

/* 360x480 gift-card art, and the gallery's portrait wallpapers — those run
   nearer 1:2, so cover takes a modest crop off a picture that is a wallpaper
   at any proportion. */
.md-typeset .ab-mag .ab-card--portrait .ab-card__media img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

/* The brand wordmarks, all drawn on one 2400x500 canvas. The mark itself sits
   in a 300px band across the middle (y=100..400) and is centred in it, so the
   set reads as a set by height while the widths run from 300px — PUBG G-COIN,
   a square icon — to 1188px, Arena Breakout's. That canvas margin is the whole
   mechanism, so the frame takes the canvas's own ratio rather than tightening
   to the marks: tightened, the square icon would come out a quarter the width
   of the widest mark and the set would stop reading as one.

   Not --photo, either. A 4.8:1 mark in a 4:3 frame letterboxes to a 540x112
   strip inside a 540x405 plate — 146px of dead panel above and below it. */
.md-typeset .ab-mag .ab-card--brand .ab-card__media img {
  aspect-ratio: 24 / 5;
}

.md-typeset .ab-mag .ab-card__copy {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding: 0 20px;
}

.ab-card--text .ab-card__copy {
  padding-top: 2px;
}

.md-typeset .ab-mag .ab-card__title {
  margin: 0 0 12px;
  padding: 0;
  border: 0;
  font-size: 26px;
  font-weight: 900;
  line-height: 1.15;
  letter-spacing: 0.75px;
  text-transform: uppercase;
  color: var(--ab-text);
}

.md-typeset .ab-mag .ab-card__title a {
  color: inherit !important;
  text-decoration: none !important;
}

.md-typeset .ab-mag .ab-card__title a:hover {
  color: var(--ab-accent-deep) !important;
}

.md-typeset .ab-mag .ab-card__excerpt {
  margin: 0 0 14px;
  font-family: var(--ab-serif);
  font-size: 17px;
  line-height: 25px;
  color: var(--ab-text-muted);
}

/* `.ab-card__lead` lived here: the dek on a card with no fold. Its last user was
   the 数码好物 cards on /tech, which now carry their sentence inside the fold
   like the 显卡 cards do, so the class and its 17/25 came out rather than sit
   dead. The 显卡 cards keep the same type through .ab-fold__summary's own
   17/25 — that is the rule to copy if a sentence-led card is ever added. */

/* `.ab-card--ec .ab-card__more { margin-top: 14px }` lived here — /topup's five
   game cards were the only --ec cards with a link row under them, and with no
   reserved strip to pin one into the row needed a real top margin of its own
   (`margin: auto 0 0` resolves to zero under .ab-list--expandable, where the
   column is only as tall as its content). Those cards lost the row on
   2026-09-21, when the five game detail pages behind it were retired and the
   price block itself became the whole card, so the rule came out with them. */

/* Text-led cards carry the only number on the page that follows the currency
   switcher, so it gets the weight of a price rather than of body copy. */
.md-typeset .ab-mag .ab-card__meta {
  margin: 0 0 14px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ab-text-muted);
}

.md-typeset .ab-mag .ab-card__meta .ab-money {
  font-size: 15px;
  font-weight: 800;
  color: var(--ab-red);
}

.md-typeset .ab-mag .ab-card__off {
  display: inline-block;
  padding: 0.08em 0.4em;
  border-radius: 999px;
  background: rgba(21, 128, 61, 0.10);
  color: var(--ab-green);
  font-weight: 800;
}

.md-typeset .ab-mag .ab-card__more {
  margin: auto 0 0;
}

.md-typeset .ab-mag .ab-card__more a,
.md-typeset .ab-mag .ab-hero__more a {
  display: inline-block;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--ab-accent);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ab-accent-deep) !important;
  text-decoration: none !important;
}

.md-typeset .ab-mag .ab-card__more a:hover,
.md-typeset .ab-mag .ab-hero__more a:hover {
  color: var(--ab-text) !important;
  border-bottom-color: var(--ab-text);
}

/* ---------- the card's action line ----------
   uncrate closes a listing entry with "Read More or Buy from X": one centred
   line, both links set alike, a small connective between them. Both grids that
   have one use this block — /tech, where the first link is the 展开 cue and
   opens the model's price rows, and the homepage's TECH cards, where the first
   link is 阅读全文 and goes to the model page. Listed together so the two
   cannot drift into looking like different components.

   (**Both names are as of when this was written.** /tech/ — the 显卡 page that
   gave this row its first user — is /gear/ since 2026-09-24 and is the 桌搭
   gallery now, so the price rows below belong to /topup/'s cards; the home
   feed's cards are copied whole out of their category page and so wear that
   page's own action line. Nothing in these rules depends on either name.)

   The fold row has a structural constraint the homepage's does not: its cue is
   what opens the fold and has to stay inside <summary> — a summary is a real
   focusable control, which is why the fold is built on <details> at all, and it
   has to keep working with JavaScript off. The buy link belongs in the same row
   but not in the summary's label, so it rides along as a sibling of the
   control. A click on an interactive descendant does not run the summary's
   activation behaviour, so the link follows without toggling.

   Colour is split the way the rest of the site splits it: --ab-accent (#ae9e85,
   uncrate's own tan) is the 1px rule, --ab-accent-deep is the text, because the
   tan is decorative and does not carry type.

   Written here, after .ab-card__more a, on purpose: that rule matches the split
   row's links too, at the same specificity, so every override below — 13px to
   15, 700 to 400, uppercase off, sans to serif — only lands because it comes
   later in the file. */
.md-typeset .ab-mag .ab-card .ab-fold__act,
.md-typeset .ab-mag .ab-card__more--split {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0 8px;
}

/* Only the fold's row is positioned; the homepage's split row is the card's
   last child in flow and carries .ab-card__more's own `margin: auto 0 0`, which
   is what pins it to the bottom of the copy column on a card the grid has
   stretched. The fold's row is pinned into the padding reserved above, against
   the same `.ab-card--fold` hook the containing block is set on, so it carries
   no margin of its own. */
.md-typeset .ab-mag .ab-card--fold .ab-fold__act {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin-top: 0;
}

.md-typeset .ab-mag .ab-card .ab-fold__cue,
.md-typeset .ab-mag .ab-fold__buy,
.md-typeset .ab-mag .ab-card__more--split a {
  margin-left: 0;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--ab-accent);
  font-family: var(--ab-serif);
  font-size: 15px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  /* Stated because the legacy `.md-typeset .ab-card span` rule sets a *ratio*,
     1.45, and a ratio resolves against each element's own font-size — so the
     cue, which sets 15px, landed at 21.75px while the link beside it inherited
     a flat 25px and the two underlines sat a pixel apart. */
  line-height: inherit;
}

.md-typeset .ab-mag .ab-fold__buy {
  color: var(--ab-accent-deep) !important;
  text-decoration: none !important;
}

.md-typeset .ab-mag .ab-fold__buy:hover {
  color: var(--ab-text) !important;
  border-bottom-color: var(--ab-text);
}

/* The separator in a row of two tappable things: 「展开 或 购买」
   on the price cards, 「查看图集 或 查询价格」 on /gear/. It carried no margin,
   so the only air around it was the one space the markup's newlines collapse
   to — measured 8px a side, and the same 8px at 1440 and at 390, because it is
   the space's own width and not a share of the card — and on 2026-10-04 the
   user asked for more. 6px a side, taking it to 14px, which puts the 或
   between the two links rather than against either. In px rather than em on
   purpose: the 16px bump further down shares these selectors and 6px reads
   right at both sizes. */
.md-typeset .ab-mag .ab-fold__or,
.md-typeset .ab-mag .ab-card__or {
  margin: 0 6px;
  font-size: 15px;
  font-style: normal;
  color: var(--ab-text-muted);
}

.md-typeset .ab-mag .ab-hero__more {
  margin: 0;
}

/* ---------- footer note ---------- */
.md-typeset .ab-mag .ab-mag__foot {
  margin: 8px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--ab-line-soft);
  font-size: 12px;
  letter-spacing: 0.3px;
  text-align: center;
  color: var(--ab-text-muted);
}

/* ---------- narrow screens ----------
   uncrate collapses .one-half to full width at 767px and drops the date
   stamps; same here. */
@media screen and (max-width: 1100px) {
  .ab-section > .ab-section__head > .ab-stamp {
    display: none;
  }
}

@media screen and (max-width: 767px) {
  .ab-list {
    grid-template-columns: minmax(0, 1fr);
  }

  .md-typeset .ab-mag .ab-hero {
    margin-bottom: 36px;
  }

  .md-typeset .ab-mag .ab-hero__title {
    font-size: 24px;
  }

  .md-typeset .ab-mag .ab-card__title {
    font-size: 22px;
  }

  .md-typeset .ab-mag .ab-hero__excerpt,
  .md-typeset .ab-mag .ab-fold__summary {
    font-size: 16px;
    line-height: 25px;
  }

  /* Restated at this weight because the card-scoped rule above is more
     specific than the one it is written beside. */
  .md-typeset .ab-mag .ab-card .ab-fold__summary {
    font-size: 16px;
  }

  /* The action line comes up with the lead above it. It is the only thing on a
     card that is meant to be tapped, so at 15px it would be the smallest type
     on the page and the hardest of them to hit. */
  .md-typeset .ab-mag .ab-fold__buy,
  .md-typeset .ab-mag .ab-card__more--split a,
  .md-typeset .ab-mag .ab-fold__or,
  .md-typeset .ab-mag .ab-card__or,
  .md-typeset .ab-mag .ab-card .ab-fold__cue {
    font-size: 16px;
  }

  .ab-section {
    margin-bottom: 40px;
  }

  .md-typeset .ab-mag .ab-section__title {
    font-size: 20px;
  }
}


/* ---------- the pricing grid's media plate --------------------------------

   /gift-cards/, /cdkeys/ and /topup/ are the site's three price pages: the
   same card the rest of the magazine draws, except that a price ladder sat
   behind a 展开 cue where the other cards carry a sentence. On 2026-10-03 they
   were restyled onto mygear.top's /rubbers/ look, at the user's request — a
   centred plate at the head of the card, the entry price inline at the end of
   the title, a square badge on the plate's corner, three to a row on a wide
   screen. The fold, the ladder, its 84px window and the footnote were left
   untouched then: rubbers has no equivalent, and the ladder is how these pages
   are read. All three have since traded the ladder for the spec picker below —
   /cdkeys/ on 2026-10-04, /gift-cards/ and /topup/ on 2026-10-05 — so no price
   card on the site folds a ladder open any more.

   Two hooks, and where each one lands is the whole design:

     ab-card--plate     on the <article>. Everything a card looks like — the
                        plate, its radius, the hover lift, the badge, the
                        inline price. It rides along when scripts/gen_home.py
                        copies a card block to the homepage, which is why the
                        homepage's eighteen price cards need no rules of their
                        own. /gear/'s twelve cards are --portrait/--lightbox
                        and carry neither class, and never see any of this.
     ab-list--pricing   on the grid <div>, once per page. Only the column count
                        and the type that follows from it — a homepage card in
                        .ab-list--feed is a --plate card in a two-column grid
                        and keeps the wider type.

   Which gives the trap worth stating: --plate on the grid and not on the
   cards — or merely forgetting to re-run gen_home.py — leaves the homepage
   with new markup and none of this styling. The badge lands as a bare word
   inside the plate, the empty price span pushes its 8px, the product shot
   letterboxes into the 24:5 wordmark frame. Nothing errors, so it has to be
   looked at.

   The plate is a fixed 240x320, the same 3:4 as the product art so that art
   fills it. It was shared with /topup/'s nineteen wordmark cards until
   2026-10-05, when those moved onto the 5:2 banner below — their 2400x500
   canvas fitted by width left the mark 30px tall in a mostly-empty frame, so
   it was worth less than a frame of its own. What is left on the plate is the
   3:4 art, on /gift-cards/ and /cdkeys/, which fills it edge for edge.

   The frame's 24/5 comes from .ab-card--brand above; this section is after it
   in the file and matches at the same 0-4-1, which is what retires it for
   these cards. Moving this block above line 1050 would put the art into a
   hairline. */
.md-typeset .ab-mag .ab-card--plate .ab-card__media {
  /* Positioned, because it is the badge's containing block and nothing else
     on the card is: without this the badge's nearest positioned ancestor is
     .ab-section, and all thirty-two of them pile into its top-left corner. */
  position: relative;
  width: 240px;
  max-width: 100%;
  aspect-ratio: 3 / 4;
  margin: 16px auto 0;
  overflow: hidden;
  border-radius: 0.55rem;
}

.md-typeset .ab-mag .ab-card--plate .ab-card__media img {
  /* Absolutely placed so the frame, not the image, decides the box: the 3:4
     shots and the 24:5 wordmarks then come out the same height, and the
     wordmark's own 24/5 rule above cannot re-open the letterbox. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: contain;
  margin: 0;
  /* The hairline the rest of the magazine rules under its media. There is no
     banner to separate here — the plate floats on the card's own white. */
  border: 0;
}

/* ---------- the wordmark cards' banner, on /topup/ ------------------------

   Every card on /topup/ carries a 2400x500 wordmark banner: the mark centred
   in a 300px band (y=100..399), its width running 300px (MIGO, PUBG G-COIN)
   to 1188px (Arena Breakout). The plate above fits that canvas *by width*,
   because 2400 is the long side — 240/2400 = 0.1 — so the mark lands 30px
   tall whatever its own width, with two thirds of the 240x320 frame empty
   above and below it.

   ab-card--wordmark gives that canvas a frame of its own ratio instead. 5:2
   is the window, in canvas px, that holds the widest mark with a margin:
   `cover` scales by height (5/2 is under the canvas's 24/5), so the visible
   strip is 1250px wide — Arena Breakout's 1188 spans 606..1793 and fits,
   centred, with ~31px either side — and the full 500px height shows, so no
   mark is cropped either way. At three columns the copy column is ~280px, so
   the mark lands 0.6 x (280/2.5) = 67px tall where the plate gave it 30.

   The frame spans the copy column — 100% less the copy's own 20px a side —
   rather than the plate's fixed 240, so the banner reads as one width with
   the text under it. Everything else about the media is the plate's: the
   radius, the position:relative the badge needs, the 16px top margin.

   Its own modifier and not `--brand`, because every price card on the site
   is --brand; only /topup/'s nineteen draw wordmarks. Rides on the article
   the way --plate and --picker do, so gen_home.py carries it to the homepage
   copies — re-run that after editing /topup/. */
.md-typeset .ab-mag .ab-card--plate.ab-card--wordmark .ab-card__media {
  width: calc(100% - 40px);
  aspect-ratio: 5 / 2;
}

.md-typeset .ab-mag .ab-card--plate.ab-card--wordmark .ab-card__media img {
  object-fit: cover;
}

/* The plate's badge sits on the picture with room to spare; the banner's does
   not — a wide mark (Arena Breakout spans ~95% of the frame) runs under a
   badge left at 10/10. The canvas keeps 100px of clear margin above the mark,
   20% of the 500px height, and the full height shows, so the top 0.2 x H of
   every banner is logo-free: ~22px at three columns. The badge is tucked into
   that strip — 4px down, sized to 16px tall — so it never meets a mark,
   whatever the mark's width. */
.md-typeset .ab-mag .ab-card--plate.ab-card--wordmark .ab-card__badge {
  top: 4px;
  left: 8px;
  padding: 2px 6px;
  font-size: 10px;
}

/* The card's own corners and lift. extra.css already rounds and lifts every
   .ab-card on the site; uncrate.css takes the radius back off with its own
   .ab-card rule at the same specificity but later in the file. The lift was
   never taken back, so /gear/ has always floated on hover and these cards are
   only being brought into line by restating both here, on --plate. */
.md-typeset .ab-mag .ab-card--plate {
  border-radius: 0.55rem;
  transition: border-color 0.12s ease, box-shadow 0.12s ease, transform 0.12s ease;
}

.md-typeset .ab-mag .ab-card--plate:hover {
  border-color: var(--ab-line);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
  transform: translateY(-1px);
}

/* The corner badge, on the plate's top-left the way rubbers sets its own.

   Two kinds of text land here. The nine cards carrying `data-discount` write
   the saving straight into the markup, the way the channel tag is written, so
   they show the same word with the script on or off. The fourteen live-platform
   cards have no such attribute — their saving is list-against-reference, it
   moves every time the FX is re-run, and typing it into the page would leave
   the card quoting a stale number — so games-prices.js overwrites their
   channel tag with the saving once prices.json lands. Until it does, and if it
   never does, they keep the channel tag: a badge that degrades to a true word
   rather than to an empty box. */
.md-typeset .ab-mag .ab-card--plate .ab-card__badge {
  position: absolute;
  z-index: 2;
  top: 10px;
  left: 10px;
  padding: 4px 8px;
  border-radius: 0;
  background: #92400e;
  color: #ffffff;
  font-family: var(--ab-sans);
  font-size: 11px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

/* The cards badging the saving itself rather than the channel — the same green
   the ladder's own .ab-ec-off pill is painted in. */
.md-typeset .ab-mag .ab-card--plate .ab-card__badge--off {
  background: var(--ab-green);
}

/* The ladder's entry-row pill, off on every plate card — the badge is the
   saving's one home here, and the two were saying the same number twice.

   It is not only de-duplication. The pill is 74px at the 15px it inherits (not
   the 13px declared: `.ab-ec-list span { font-size: inherit }` is 0-4-1 against
   `.ab-ec-off`'s 0-3-0), and three columns cap the copy box at 265px however
   wide the window is, because .ab-mag is max-width 960px. On the seven cards
   whose labels are long — "3600 Diamonds", "43500 Coins" — the label column
   takes 105-130px of that and leaves the price 111-135px for a 135px row. The
   row wrapped, so the entry line stood 76px tall against the 36.75px it was
   measured at, the 84px clip lost its second rung, and the pill split at its
   own hyphen with "25%" landing under the 展开 或 购买 row. Hidden, the
   entry row is `label | $x.xx.` and every row on all 32 cards is one line. */
.md-typeset .ab-mag .ab-card--plate .ab-ec-off {
  display: none;
}

/* The entry tier, inline at the end of the title. CSS cannot move the ladder's
   own first price up here (the title's hypothetical width is the whole line,
   so a flex row pushes the price onto the next one instead of into it), which
   is why the markup carries an empty span and games-prices.js fills it in the
   same tick it draws the ladder. */
.md-typeset .ab-mag .ab-card--plate .ab-card__price {
  display: inline;
  margin: 0 0 0 8px;
  font-size: 18px;
  font-weight: 900;
  /* 0, not the inherited ratio. At 18px the legacy `.ab-card span` rule
     (extra.css) computes a 26.1px line box, and a box that tall sits its
     descender ~0.65px below the 29.9px line's own — which lifts every title
     line the price rides on by that much, so a two-line title measures 61
     against the 59.8 the reservation above sets and the row goes 1px ragged.
     A zero line box contributes only (ascent - descent)/2 above the baseline
     and nothing below. Only the box shrinks; the digits are drawn on the
     baseline either way. */
  line-height: 0;
  letter-spacing: 0;
  color: var(--ab-red);
  white-space: nowrap;
}

/* Load-bearing. The span is empty until the fetch lands, and an empty inline
   box still pays its margin — every title on the page would trail 8px of
   nothing while the ladder is in flight, and forever with the script off. */
.md-typeset .ab-mag .ab-card--plate .ab-card__price:empty {
  display: none;
}

/* Three to a row, and only from 1024px up. The ladder is a two-column grid of
   its own — `max-content minmax(0, 1fr)` with a 1.1em gutter, see .ab-ec-list
   above — whose longest row needs about 227px, and at three columns the copy
   column measures 264px at every width from 1024 up (.ab-mag is capped at 960,
   so the columns stop growing well before the viewport does). That leaves 37px
   of slack. Two columns at 760px leave the copy column 189px, which is the
   same ladder wrapping its own price onto a second line, so the breakpoint
   cannot be the 768 the single-column rule uses.

   The title comes down with it: 26px was measured against a 319px column, and
   in 264px with a price now riding at the end of it the long names —
   "PlayStation Network 充值卡（美国）", "Nintendo Switch Online 会员资格" —
   would run to four lines. The two figures move together so the price stays
   proportionally under the title, the way rubbers sets 14 against 17. */
@media screen and (min-width: 1024px) {
  .md-typeset .ab-mag .ab-list--pricing {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .md-typeset .ab-mag .ab-list--pricing .ab-card__title {
    font-size: 17px;
  }

  .md-typeset .ab-mag .ab-list--pricing .ab-card__price {
    margin-left: 6px;
    font-size: 14px;
  }
}


/* ---------- the spec picker, on all three price pages --------------------

   On 2026-10-04 the user had /cdkeys/ read like mygear.top's /rubbers/ card:
   a labelled <select> where the price ladder used to be, and, in the same
   request, the picture the size of rubbers' own. The two halves are one change
   — the picker is what the square was for.

   The square went back in the next request. "The picture on /cdkeys/ the same
   size as the one on /gift-cards/" — so the 240x240 frame and the `cover` /
   `object-position: top` on the image came out, and this block no longer
   touches .ab-card__media at all. The square and its crop were a matched pair:
   a square holding 360x480 art only fills with `cover`, and `cover` in a
   square is precisely what made the picture smaller than the art it held.
   Both pages now take the plate above — 240x320 at 3/4 with `contain` — and
   all seven files are 360x480 exactly, so contain draws every one of them at
   the frame's full 240x320, nothing letterboxed, which is what /gift-cards/
   was already doing. Measured, both pages, in _tmp_ocr/probe_frame_size.py.

   ab-card--picker rides on the <article>, the way --plate does, so the six
   /cdkeys/ cards scripts/gen_home.py copies to the homepage take the picker
   along with them. Scoping this to the grid div instead would leave those six
   with the new markup and none of this styling — an unstyled <select> — and
   re-running gen_home.py is what carries the markup over in the first place.

   Two things this block does, and the one it deliberately does not re-open:

   1. The ladder's window, switched off. --fold clips .ab-ec-list to 84px under
      a 22px fade and --shop pins that height, both because a ladder runs past
      the window. The picker is a single control and must not be clipped. This
      block is written after both and matches at the same 0-4-0, which is what
      retires them here. The 46px .ab-card__copy reserves for the action line
      is --fold's, and staying is the point: the CTA keeps its place at the
      foot of the card, one 购买 under the picker instead of under a
      ladder. --fold also keeps .ab-ec-foot hidden, which is how these cards
      already read; the picker does not bring it back.

   2. The label. `.ab-ec-list span { font-size: inherit }` above is 0-4-1 and
      would pull the label up to the block's 15px, so these rules are written
      a class longer — the same trap .ab-ec-off documents under --plate.

   Two rules on .ab-card__media used to stand here, the square and the crop,
   and the plate above is what answers for the frame again now that they are
   gone — this block no longer touches the frame.

   What it does touch is the copy's top edge, and that one is the same
   subtraction. All three price pages used to get their air under the picture
   from a <p class="ab-cat"> line — 51px from frame to title, measured on
   2026-10-04 — and /cdkeys/ lost that line the same day it lost its blurb,
   which left the frame sitting directly on the title: 0px, measured, and the
   user asked for the interval back. 16px, the frame's own top margin, so the
   picture ends up floating in equal white above and below. /gift-cards/ and
   /topup/ followed onto the picker on 2026-10-05 and gave up their own ab-cat
   lines in the same edit, so all three take this padding and no price card
   draws the cat line any more. Padding on the column rather than a margin on
   the frame, so the frame rule above stays a rule about the frame; the act row
   is pinned to this column's bottom edge and rides down with it, which is
   where it should be. */

.md-typeset .ab-mag .ab-card--picker .ab-card__copy {
  padding-top: 16px;
}

/* --ec makes the block a two-column grid, --fold clips it, --shop pins it.
   Back to a plain centred box: what lands inside is one label and one
   <select>, and the select is sized by its own rule rather than by a column. */
.md-typeset .ab-mag .ab-card--picker .ab-ec-list {
  display: block;
  height: auto;
  max-height: none;
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
  text-align: center;
}

.md-typeset .ab-mag .ab-card--picker .ab-fold__summary {
  /* It is a <div> on these cards, not a <summary>, so there is nothing to
     toggle. The row's only in-flow child is gone as well — the lead came out
     of all seven on 2026-10-04, leaving the action row, which is absolute —
     so this now collapses to nothing and the copy runs title, spec, CTA. */
  cursor: default;
}

.md-typeset .ab-mag .ab-card--picker .ab-ec-pick {
  display: block;
}

/* rubbers labels its own control OPTION, in caps over a full-width select.
   Chinese has no case to lean on, so the label carries the weight instead. */
.md-typeset .ab-mag .ab-card--picker .ab-ec-pick .ab-ec-pick__label {
  display: block;
  margin: 0 0 6px;
  font-family: var(--ab-sans);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.08em;
  color: var(--ab-text-muted);
}

/* Capped at the plate's own width rather than run to the card's edges: on
   /cdkeys/ the copy column is 265px in three columns so the cap barely shows,
   and on the homepage the same card sits in a 470px feed card, where a select
   spanning the whole of it would read as a form field rather than as the
   control under a 240px picture. */
.md-typeset .ab-mag .ab-card--picker .ab-ec-pick .ab-ec-pick__select {
  display: block;
  width: 100%;
  max-width: 240px;
  margin: 0 auto;
  padding: 8px 10px;
  border: 1px solid var(--ab-line);
  border-radius: 0.25rem;
  background: #ffffff;
  color: var(--ab-text);
  /* Stated, not inherited: a <select> takes neither the family nor the size
     from its parent unless it is told to. */
  font-family: var(--ab-sans);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
}

.md-typeset .ab-mag .ab-card--picker .ab-ec-pick__select:hover {
  border-color: var(--ab-text-muted);
}

.md-typeset .ab-mag .ab-card--picker .ab-ec-pick__select:focus {
  outline: 2px solid rgba(111, 98, 80, 0.35);
  outline-offset: 1px;
  border-color: var(--ab-accent-deep);
}
