/* ============================================================
   Card Shop - Frontend Styles
   Replicates mailmajiang.com design. Mobile-first responsive.
   ============================================================ */

/* ----- CSS Variables ----- */
:root {
    --header-bg: #393d49;
    --header-height: 50px;
    --body-bg: #faf7f2;
    --teal: #009688;
    --teal-dark: #00796b;
    --warm-btn: #ffb800;
    --warm-btn-hover: #e6a600;
    --price-color: #ff4400;
    --text-color: #333;
    --text-muted: #666;
    --text-light: #888;
    --border-color: #e6e6e6;
    --table-header-bg: #f2f2f2;
    /* Image surfaces. The plate tint is pulled from --body-bg rather than a neutral
       grey so a letterboxed or transparent upload sits on the same warm paper as
       the rest of the page; the icon tone is deliberately low-contrast so a missing
       image reads as an absence, not as a filled-in box competing with the name. */
    --thumb-bg: #f4f1ec;
    --thumb-icon: #c9c2b6;
    --card-bg: #fff;
    --footer-bg: #f1f1f1;
    --quote-bg: #fff;
    --container-width: 1170px;
    /* Vertical rhythm. Two steps only: --section-gap separates the major bands
       of a page (product grid / articles / tags / contact), --block-gap the
       cards and rows inside one band. Stated once here so the spacing between
       sections is the same number everywhere instead of a per-rule guess. */
    --section-gap: 25px;
    --block-gap: 20px;
    /* Ceiling for editor-authored media (see "Rich Text Content"). Viewport
       relative on purpose: a tall image scales down to something that still
       leaves the rest of the page visible on whatever screen is reading it. */
    --rich-media-max-h: 70vh;
    /* Product detail image plate. Deliberately one number: it is the value most likely
       to be retuned by taste, and the three breakpoint overrides below scale with it.
       For reference, the identity block beside it (badge and stock, title, price) is
       about 115px tall, so a value near that reads as its equal, and above it the
       image becomes the dominant element of the row. */
    --pd-image-size: 150px;
    --font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", Tahoma, Arial, sans-serif;
    --shadow-sm: 0 1px 2px rgba(0,0,0,.06);
    --shadow: 0 1px 6px rgba(0,0,0,.08);
    --radius: 2px;
}

/* ----- Reset & Base ----- */
*, *::before, *::after { box-sizing: border-box; }
body {
    margin: 0;
    padding: 0;
    background: var(--body-bg);
    color: var(--text-color);
    font: 14px/1.6 var(--font-family);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    -webkit-font-smoothing: antialiased;
}
a { color: var(--teal); text-decoration: none; }
a:hover { color: var(--teal-dark); }
img { max-width: 100%; height: auto; }
h1, h2, h3, h4, h5, h6 { margin: 0 0 .5em; font-weight: 600; color: var(--text-color); }

/* ----- Layout ----- */
.container {
    width: 100%;
    max-width: var(--container-width);
    margin: 0 auto;
    padding: 0 15px;
}

/* ----- Header ----- */
.site-header {
    background: var(--header-bg);
    height: var(--header-height);
    line-height: var(--header-height);
    position: sticky;
    top: 0;
    z-index: 1000;
    box-shadow: 0 1px 4px rgba(0,0,0,.2);
}
.site-header .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 100%;
}
.site-logo {
    display: flex;
    align-items: center;
    gap: 8px;
    color: #fff;
    font-size: 18px;
    font-weight: 600;
    white-space: nowrap;
}
/* A logo upload is whatever shape the operator saved. Height is fixed so the
   bar keeps its 50px rhythm; width follows the file's own ratio and is capped
   so a wide banner cannot push the nav off the right edge. */
.site-logo img {
    height: 30px;
    width: auto;
    max-width: 180px;
    object-fit: contain;
    display: block;
    flex-shrink: 0;
}
.site-logo:hover { color: #fff; }
.site-logo .logo-icon {
    width: 30px; height: 30px;
    background: var(--teal);
    border-radius: 4px;
    display: flex; align-items: center; justify-content: center;
    font-size: 16px; color: #fff; font-weight: 700;
}

/* Desktop Nav */
.site-nav {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
    gap: 0;
}
.site-nav li a {
    display: block;
    color: rgba(255,255,255,.7);
    padding: 0 18px;
    font-size: 14px;
    height: var(--header-height);
    line-height: var(--header-height);
    transition: color .2s, background .2s;
}
.site-nav li a:hover,
.site-nav li a.active {
    color: #fff;
    background: rgba(255,255,255,.08);
}

/* Mobile menu toggle */
.menu-toggle {
    display: none;
    background: none;
    border: none;
    color: #fff;
    font-size: 22px;
    cursor: pointer;
    padding: 6px 10px;
}

/* Mobile Nav */
.mobile-nav {
    display: none;
    background: var(--header-bg);
    position: fixed;
    top: var(--header-height);
    left: 0;
    right: 0;
    z-index: 999;
    border-top: 1px solid rgba(255,255,255,.1);
    box-shadow: 0 4px 8px rgba(0,0,0,.2);
}
.mobile-nav.open { display: block; }
.mobile-nav ul {
    list-style: none;
    margin: 0;
    padding: 0;
}
.mobile-nav ul li a {
    display: block;
    color: rgba(255,255,255,.8);
    padding: 12px 20px;
    border-bottom: 1px solid rgba(255,255,255,.06);
    font-size: 14px;
}
.mobile-nav ul li a:hover,
.mobile-nav ul li a.active {
    color: #fff;
    background: rgba(255,255,255,.05);
}

/* ----- H1 SEO (hidden) ----- */
.seo-h1 {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    border: 0;
}

/* ----- Main Content ----- */
.main-container {
    flex: 1;
    padding: 15px 0 30px;
}

/* ----- Announcement / Quote ----- */
.site-quote {
    background: var(--quote-bg);
    border-left: 5px solid var(--teal);
    padding: 15px 20px;
    /* The shorthand matters: a <blockquote> carries `margin: 1em 40px` from the browser's
       own stylesheet, and setting only margin-bottom left that 40px indent in place — the
       announcement sat 40px to the right of every other block on the page. */
    margin: 0 0 var(--block-gap);
    color: var(--text-muted);
    font-size: 14px;
    line-height: 1.8;
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}
.site-quote strong { color: var(--price-color); }
.site-quote a { color: var(--teal); font-weight: 500; }

/* The announcement is editor-authored now, so it arrives as HTML rather than a run of
   newlines. Keep its blocks from adding margin at the top and bottom of the plate, and
   hold any image the operator drops in to a sensible size. */
.site-quote > *:first-child { margin-top: 0; }
.site-quote > *:last-child { margin-bottom: 0; }
.site-quote p { margin: 0 0 8px; }
.site-quote h1, .site-quote h2, .site-quote h3,
.site-quote h4, .site-quote h5, .site-quote h6 {
    margin: 0 0 8px;
    font-size: 15px;
    color: var(--text-color);
}
.site-quote ul, .site-quote ol { margin: 0 0 8px; padding-left: 22px; }
.site-quote img {
    max-width: 100%;
    height: auto !important;
    max-height: 220px;
    object-fit: contain;
    border-radius: var(--radius);
}
.site-quote table { display: block; max-width: 100%; overflow-x: auto; }

/* Category page header. The flex row lives on a separate class so the plain
   announcement quote, which is free-flowing multi-line text, is untouched. */
.site-quote.cat-quote {
    display: flex;
    align-items: center;
    gap: 14px;
}
.cat-quote .cat-thumb-lg {
    width: 40px;
    height: 40px;
    border: 1px solid var(--border-color);
}
.cat-quote-text { min-width: 0; }

/* ----- Product Table (Desktop) ----- */
.product-table-section { margin-bottom: var(--block-gap); }

.product-table {
    width: 100%;
    border-collapse: collapse;
    background: var(--card-bg);
    box-shadow: var(--shadow-sm);
    border-radius: var(--radius);
    overflow: hidden;
    border: 1px solid var(--border-color);
}
.product-table thead th {
    background: var(--table-header-bg);
    color: var(--text-muted);
    font-weight: 600;
    font-size: 14px;
    padding: 10px 15px;
    text-align: left;
    border-bottom: 1px solid var(--border-color);
}
.product-table thead th:first-child {
    color: var(--text-color);
    font-size: 15px;
}
.product-table tbody td {
    padding: 12px 15px;
    border-bottom: 1px solid #f0f0f0;
    font-size: 14px;
    color: var(--text-muted);
    vertical-align: middle;
}
.product-table tbody tr:last-child td { border-bottom: none; }
.product-table tbody tr:hover { background: #fafafa; }

.product-table .prod-name-cell {
    display: flex;
    align-items: center;
    gap: 10px;
    max-width: 500px;
}
/* Thumbnail slot. Image and placeholder share every box property so the product
   names line up down the column whether or not a row has an image. */
.product-table .prod-thumb,
.product-table .prod-thumb-ph {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    border-radius: var(--radius);
    border: 1px solid var(--border-color);
    background: var(--thumb-bg);
}
.product-table .prod-thumb {
    object-fit: cover;
    display: block;
}
.product-table .prod-thumb-ph {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--thumb-icon);
}
.product-table .prod-thumb-ph svg {
    width: 22px;
    height: 22px;
}
.product-table .prod-name {
    color: var(--text-color);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    line-height: 1.5;
}
.product-table .prod-name:focus-visible {
    outline: 2px solid var(--teal);
    outline-offset: 2px;
}
.product-table .prod-price {
    white-space: nowrap;
    font-weight: 500;
}
.product-table .stock-num { font-family: monospace; }

/* Badge */
.badge-auto {
    display: inline-block;
    background: var(--teal);
    color: #fff;
    font-size: 12px;
    padding: 1px 6px;
    border-radius: var(--radius);
    vertical-align: middle;
}

/* Buy button (table) */
.btn-buy-sm {
    display: inline-block;
    background: var(--warm-btn);
    color: #fff;
    padding: 5px 20px;
    border-radius: var(--radius);
    font-size: 14px;
    border: none;
    cursor: pointer;
    transition: background .2s;
    white-space: nowrap;
}
.btn-buy-sm:hover { background: var(--warm-btn-hover); color: #fff; }

/* ----- Category Thumbnail -----
   One square mark reused at three sizes: table head (20px), mobile section
   heading (22px), tag cloud (16px). Sized by the host context rather than by a
   modifier per site, so a category reads the same everywhere it appears. */
.cat-thumb {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    border-radius: var(--radius);
    object-fit: cover;
    background: var(--thumb-bg);
    display: block;
}
/* Placeholder that holds a thumbnail slot open for a category with no image, so
   names in the same list keep one left edge. Deliberately faint — it is spacing
   that happens to be visible, not a missing-image warning. */
.cat-thumb-empty {
    display: inline-block;
    flex-shrink: 0;
    border-radius: var(--radius);
    background: var(--thumb-bg);
    opacity: .5;
}
.cat-head {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

/* ----- Product Card Grid ----- */
.card-section-title {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 16px;
    font-weight: 600;
    color: var(--text-color);
    margin: var(--section-gap) 0 15px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border-color);
}
.card-section-title .cat-thumb {
    width: 22px;
    height: 22px;
}

.product-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 15px;
    margin-bottom: var(--block-gap);
}
.product-card {
    background: var(--card-bg);
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    transition: box-shadow .2s;
}
.product-card:hover { box-shadow: var(--shadow); }
.product-card a { color: inherit; display: block; }
.product-card a:focus-visible {
    outline: 2px solid var(--teal);
    outline-offset: -2px;
}
/* Square, cropped, never distorted. aspect-ratio reserves the box before the
   image arrives, so a grid of cards does not reflow as thumbnails load. */
.product-card .card-img,
.product-card .card-img-placeholder {
    width: 100%;
    aspect-ratio: 1 / 1;
    background: var(--thumb-bg);
    border-bottom: 1px solid var(--border-color);
}
.product-card .card-img {
    object-fit: cover;
    display: block;
}
.product-card .card-img-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--thumb-icon);
}
.product-card .card-img-placeholder svg {
    width: 34%;
    max-width: 52px;
    height: auto;
}
.product-card .card-body {
    padding: 10px 12px;
}
.product-card .card-badge {
    margin-bottom: 6px;
}
.product-card .card-title {
    font-size: 13px;
    color: var(--text-color);
    line-height: 1.5;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin-bottom: 8px;
    min-height: 39px;
}
.product-card .card-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.product-card .card-stock {
    font-size: 12px;
    color: var(--text-light);
}
.product-card .card-price {
    font-size: 15px;
    color: var(--price-color);
    font-weight: 700;
}

/* ----- Two-Column Layout (articles below products) ----- */
.two-col-section {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--block-gap);
    margin-bottom: var(--block-gap);
}
.article-block {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    overflow: hidden;
}
.article-block-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 15px;
    border-bottom: 1px solid var(--border-color);
    background: #fafafa;
}
.article-block-header h3 {
    font-size: 15px;
    margin: 0;
    color: var(--text-color);
}
.article-block-header a {
    font-size: 13px;
    color: var(--text-light);
}
.article-block-header a:hover { color: var(--teal); }

.article-list {
    list-style: none;
    margin: 0;
    padding: 0;
    counter-reset: article-counter;
}
.article-list li {
    counter-increment: article-counter;
    display: flex;
    align-items: baseline;
    padding: 10px 15px;
    border-bottom: 1px solid #f5f5f5;
    font-size: 13px;
    gap: 8px;
}
.article-list li:last-child { border-bottom: none; }
.article-list li::before {
    content: counter(article-counter);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--table-header-bg);
    color: var(--text-muted);
    font-size: 12px;
    flex-shrink: 0;
}
.article-list li:nth-child(-n+3)::before {
    background: var(--teal);
    color: #fff;
}
.article-list li a {
    flex: 1;
    color: var(--text-color);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.article-list li a:hover { color: var(--teal); }
.article-list li .date {
    color: var(--text-light);
    font-size: 12px;
    flex-shrink: 0;
}

/* "Read all" button */
.btn-all-articles {
    display: block;
    max-width: 300px;
    margin: var(--block-gap) auto;
    text-align: center;
    background: var(--warm-btn);
    color: #fff;
    padding: 8px 30px;
    border-radius: var(--radius);
    font-size: 14px;
    border: none;
    cursor: pointer;
    transition: background .2s;
}
.btn-all-articles:hover { background: var(--warm-btn-hover); color: #fff; }

/* ----- Tag Cloud ----- */
/* Sits between two major bands, so it takes the section step at the top. That
   matches the .mt-3 the home template already puts on it: the band spacing is
   now correct from the stylesheet alone, with or without the utility class. */
.tag-cloud {
    margin: var(--section-gap) 0 var(--block-gap);
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.tag-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    font-size: 12px;
    color: var(--text-muted);
    background: var(--card-bg);
    cursor: pointer;
    transition: all .2s;
}
.tag-item:hover {
    border-color: var(--teal);
    color: var(--teal);
}
.tag-item:focus-visible {
    outline: 2px solid var(--teal);
    outline-offset: 2px;
}
/* A tag carrying a mark keeps the same overall height; the mark eats into the
   left padding instead of making the pill taller than its plain neighbours. */
.tag-item .cat-thumb {
    width: 16px;
    height: 16px;
    margin-left: -5px;
}

/* ----- Contact QR Section ----- */
.contact-section {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    padding: 25px;
    margin: var(--section-gap) 0;
    display: flex;
    align-items: center;
    gap: var(--block-gap);
}
/* Fixed square slot: the code is normally square, but a screenshot with a
   caption strip is not, and contain letterboxes that onto the plate tint
   instead of squashing the code into something a phone cannot read. */
.contact-section .qr-img {
    width: 96px;
    height: 96px;
    border-radius: 4px;
    flex-shrink: 0;
    object-fit: contain;
    background: var(--thumb-bg);
    display: block;
}
.contact-section .contact-text { font-size: 14px; }
.contact-section .contact-title {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-color);
    margin-bottom: 2px;
}
.contact-section .contact-sub {
    font-size: 15px;
    color: var(--teal);
    font-weight: 500;
    margin-bottom: 4px;
}
.contact-section .contact-hint {
    font-size: 13px;
    color: var(--text-light);
}

/* ----- Footer ----- */
.site-footer {
    background: var(--footer-bg);
    padding: 20px 0;
    text-align: center;
    color: var(--text-light);
    font-size: 13px;
    margin-top: auto;
}

/* ----- Fixed Action Buttons ----- */
.fab-group {
    position: fixed;
    right: 20px;
    bottom: 30px;
    z-index: 900;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.fab-btn {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    box-shadow: 0 2px 8px rgba(0,0,0,.15);
    transition: transform .2s;
}
.fab-btn:hover { transform: scale(1.1); }
.fab-contact { background: var(--teal); color: #fff; }
.fab-top { background: var(--card-bg); color: var(--text-muted); border: 1px solid var(--border-color); }

/* ============================================================
   Product Detail Page
   ============================================================ */
.pd-layout {
    display: grid;
    grid-template-columns: 1fr 340px;
    gap: var(--block-gap);
    margin-bottom: var(--section-gap);
}
/* A grid item's min-width defaults to auto — its content's minimum size — so a wide
   child stretches the track itself. That defeats the `overflow-x: auto` on tables and
   <pre> inside editor-authored content: the block never scrolls, it just widens the
   column and pushes the whole page sideways. Measured with a 400-character unbroken
   token in a <pre>: the document scrolled to 3293px against a 1265px viewport, and
   every form control in the left column was dragged out with it. Zeroing the minimum
   is what lets the caps further down actually bite. */
.pd-main,
.pd-sidebar {
    min-width: 0;
}

/* Product image + identity row.
   The image is letterboxed inside a fixed square plate: a 3000px-tall poster and
   a wide banner both end up the same size, neither distorted (contain, not cover)
   nor able to push the price below the fold. With no image the row collapses to
   just .pd-head-info at full width, which is the pre-image layout exactly. */
.pd-head {
    display: flex;
    align-items: flex-start;
    gap: 20px;
    margin-bottom: 18px;
}
.pd-head-info {
    flex: 1;
    min-width: 0;
}
.pd-head .pd-price-row { margin-bottom: 0; }

/* Deliberately modest. This is an identity mark next to the name, stock and price —
   not a product photo the buyer is meant to study — so it should not out-weigh the
   text it sits beside or push the price down the page. Measured at 1265px wide the
   old 200px plate took 27% of the column; 120px takes 16%. */
/* Sized to the identity block beside it. That block — badge and stock, title, price —
   measures ~115px tall, so a 112px plate reads as its partner instead of a small mark
   floating next to a taller column. */
.pd-image-wrap {
    width: var(--pd-image-size);
    height: var(--pd-image-size);
    flex-shrink: 0;
    padding: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--thumb-bg);
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    overflow: hidden;
}
.pd-image {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    /* Flex items get min-width/min-height:auto, which can stop a very tall image
       from shrinking below its intrinsic size and let it spill out of the plate. */
    min-width: 0;
    min-height: 0;
    object-fit: contain;
    display: block;
}

/* Product info card */
.pd-info {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    padding: 20px;
    margin-bottom: var(--block-gap);
}
.pd-meta {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 12px;
    font-size: 13px;
}
.pd-stock { color: var(--text-muted); }
.pd-title {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-color);
    line-height: 1.6;
    margin-bottom: 15px;
    /* A pasted product name can be one unbroken token; it wraps rather than
       stretching .pd-head-info and squeezing the image plate. */
    overflow-wrap: break-word;
}
.pd-price-row {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin-bottom: 20px;
}
.pd-price {
    font-size: 26px;
    font-weight: 700;
    color: var(--price-color);
}
.pd-orig-price {
    font-size: 14px;
    color: var(--text-light);
    text-decoration: line-through;
}

/* Wholesale table */
.wholesale-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 15px;
    font-size: 13px;
}
.wholesale-table th,
.wholesale-table td {
    padding: 6px 10px;
    border: 1px solid var(--border-color);
    text-align: center;
}
.wholesale-table th {
    background: var(--table-header-bg);
    color: var(--text-muted);
    font-weight: 500;
}
.wholesale-table .active-tier { background: #e8f5e9; }

/* Order form */
.pd-form { margin-top: 10px; }
.pd-form .form-group {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
}
.pd-form .form-label {
    width: 80px;
    font-size: 14px;
    color: var(--text-muted);
    flex-shrink: 0;
}
.pd-form .form-input {
    flex: 1;
    /* A flex item's min-width defaults to auto, which for a text input is its
       size attribute and for a select its widest option — either one can hold
       the control wider than its share of the row. Zeroing it makes all five
       controls resolve to the same width. */
    min-width: 0;
    height: 38px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    padding: 0 12px;
    font-size: 14px;
    font-family: var(--font-family);
    outline: none;
    transition: border-color .2s;
}
.pd-form .form-input:focus { border-color: var(--teal); }
.pd-form .form-input.is-invalid { border-color: var(--price-color); }

/* Validation message and the Turnstile widget both sit in the control column, so
   they are indented past the label rather than starting at the row's left edge.
   The 80px matches .form-label's width, and steps down with it at 480px. */
.pd-form .form-error {
    color: var(--price-color);
    font-size: 13px;
    margin: -8px 0 10px 80px;
}
.pd-form .form-turnstile {
    margin: 12px 0 12px 80px;
    /* Turnstile renders a fixed 300px-wide iframe it will not shrink. Let the block
       scroll rather than let the widget push the page sideways. */
    max-width: 100%;
    overflow-x: auto;
}

.btn-submit {
    display: block;
    width: 100%;
    padding: 10px 0;
    background: var(--teal);
    color: #fff;
    border: none;
    border-radius: var(--radius);
    font-size: 15px;
    font-weight: 500;
    cursor: pointer;
    transition: background .2s;
    margin-top: 5px;
    font-family: var(--font-family);
}
.btn-submit:hover { background: var(--teal-dark); }
.btn-submit:disabled { opacity: .6; cursor: not-allowed; }

/* Product description tabs */
.pd-tabs {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    overflow: hidden;
}
.pd-tab-header {
    display: flex;
    border-bottom: 1px solid var(--border-color);
}
.pd-tab-header span {
    padding: 10px 20px;
    font-size: 14px;
    color: var(--text-muted);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.pd-tab-header span.active {
    color: var(--teal);
    border-bottom-color: var(--teal);
}
.pd-tab-content {
    padding: 20px;
    line-height: 1.8;
    font-size: 14px;
    color: var(--text-muted);
}
.pd-tab-content p { margin: 0 0 10px; }
.pd-tab-content strong { color: var(--text-color); }

/* Sidebar article list */
.sidebar-card {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    overflow: hidden;
    margin-bottom: 15px;
}
.sidebar-card-header {
    padding: 10px 15px;
    border-bottom: 1px solid var(--border-color);
    background: #fafafa;
    font-size: 15px;
    font-weight: 600;
    color: var(--text-color);
}
.sidebar-article-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.sidebar-article-list li {
    padding: 8px 15px;
    border-bottom: 1px solid #f5f5f5;
}
.sidebar-article-list li:last-child { border-bottom: none; }
.sidebar-article-list li a {
    display: block;
    font-size: 13px;
    color: var(--text-color);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sidebar-article-list li a:hover { color: var(--teal); }
.sidebar-article-list li .date {
    font-size: 12px;
    color: var(--text-light);
}

/* ============================================================
   Payment Modal
   ============================================================ */
/* Payment method chooser, shown expanded instead of collapsed into a <select>, so
   the buyer sees every gateway without an extra tap. The radio input stays the real
   control and keeps the group in the accessibility tree; it is visually replaced by
   .pay-option-inner, which is styled from the adjacent-sibling :checked state so no
   :has() support is required. */
.pd-form .form-group-block { align-items: flex-start; }
.pd-form .form-group-block .form-label { padding-top: 9px; }

.pay-options {
    flex: 1;
    min-width: 0;
    display: flex;
    /* One row, always. The tiles share the width evenly and shrink together; when
       there are too many gateways to fit a phone, the strip scrolls sideways inside
       its own box rather than wrapping into a column or widening the page. */
    flex-wrap: nowrap;
    gap: 10px;
    overflow-x: auto;
    padding-bottom: 2px;
    scrollbar-width: thin;
}
.pay-option {
    display: block;
    cursor: pointer;
    /* Equal share of the row, but never so narrow that the label is unreadable or the
       tile untappable — below that width the container scrolls instead. */
    flex: 1 1 0;
    min-width: 104px;
}
.pay-radio {
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
}
.pay-option-inner {
    display: flex;
    align-items: center;
    gap: 10px;
    height: 100%;
    min-height: 48px;
    padding: 8px 12px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    background: var(--card-bg);
    transition: border-color .2s, background .2s;
}
.pay-option:hover .pay-option-inner { border-color: var(--teal); }
.pay-radio:checked + .pay-option-inner {
    border-color: var(--teal);
    background: #f0fdfa;
    box-shadow: inset 0 0 0 1px var(--teal);
}
/* The input is invisible, so the focus ring has to be drawn on the tile or the
   group becomes unusable by keyboard. */
.pay-radio:focus-visible + .pay-option-inner {
    outline: 2px solid var(--teal);
    outline-offset: 2px;
}
/* The marks carry their own colour, so this is just a fixed box to hold one. */
.pay-icon {
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.pay-svg {
    width: 100%;
    height: 100%;
    display: block;
}
.pay-label {
    font-size: 13px;
    line-height: 1.3;
    color: var(--text-color);
    min-width: 0;
    /* Keep the row honest: a long gateway name is clipped rather than allowed to
       wrap and make one tile taller than its neighbours. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ============================================================
   Order Pages
   ============================================================ */
.page-card {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    max-width: 600px;
    margin: 0 auto var(--block-gap);
    overflow: hidden;
}
.page-card-header {
    padding: 12px 20px;
    border-bottom: 1px solid var(--border-color);
    background: #fafafa;
    font-size: 16px;
    font-weight: 600;
}
.page-card-body { padding: 20px; }

.order-info-table {
    width: 100%;
    font-size: 14px;
}
.order-info-table th {
    padding: 8px 12px;
    text-align: left;
    color: var(--text-muted);
    font-weight: 500;
    width: 100px;
    vertical-align: top;
}
.order-info-table td {
    padding: 8px 12px;
    color: var(--text-color);
    /* Order numbers and gateway trade ids are long unbroken tokens. */
    overflow-wrap: break-word;
}

.order-status {
    display: inline-block;
    padding: 2px 10px;
    border-radius: var(--radius);
    font-size: 13px;
    font-weight: 500;
}
.order-status.pending { background: #fff3cd; color: #856404; }
.order-status.paid { background: #d1e7dd; color: #0f5132; }
.order-status.expired { background: #e2e3e5; color: #41464b; }
.order-status.closed { background: #f8d7da; color: #842029; }

.card-content-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.card-content-list li {
    padding: 8px 12px;
    border-bottom: 1px solid #f0f0f0;
    font-family: 'Consolas', 'Monaco', monospace;
    font-size: 13px;
    word-break: break-all;
}
.card-content-list li:last-child { border-bottom: none; }

/* ============================================================
   Article Pages
   ============================================================ */
.article-page-list {
    display: grid;
    gap: 15px;
    margin-bottom: var(--block-gap);
}
.article-page-item {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    padding: 15px 20px;
    display: flex;
    align-items: baseline;
    gap: 15px;
}
.article-page-item .title {
    flex: 1;
    font-size: 15px;
    color: var(--text-color);
}
.article-page-item .title:hover { color: var(--teal); }
.article-page-item .meta {
    font-size: 13px;
    color: var(--text-light);
    white-space: nowrap;
}

.article-detail {
    background: var(--card-bg);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-color);
    padding: 30px;
    margin-bottom: var(--block-gap);
}
.article-detail h1 {
    font-size: 22px;
    margin-bottom: 10px;
    overflow-wrap: break-word;
}
.article-detail .meta {
    font-size: 13px;
    color: var(--text-light);
    margin-bottom: var(--block-gap);
    padding-bottom: 15px;
    border-bottom: 1px solid var(--border-color);
}

/* Cover image.
   The slot owns the ratio, not the file. aspect-ratio reserves the band before
   the image arrives, so the article text does not jump down as it loads, and it
   fixes the height outright instead of capping it with max-height: on a replaced
   element a violated max-height shrinks the *width* to keep the ratio, which is
   why a tall cover used to render narrow and adrift of the text below it.
   The image then fills the slot and crops, never stretches. */
.article-detail .article-cover {
    aspect-ratio: 2 / 1;
    max-height: 400px;
    margin-bottom: var(--block-gap);
    border-radius: 4px;
    overflow: hidden;
    background: var(--thumb-bg);
}
.article-detail .article-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.article-detail .content {
    font-size: 15px;
    line-height: 1.8;
    color: var(--text-muted);
}
.article-detail .content h2 { font-size: 18px; margin: 20px 0 10px; }
.article-detail .content h3 { font-size: 16px; margin: 15px 0 8px; }
.article-detail .content p { margin: 0 0 12px; }
/* Images, code, quotes, tables and embeds inside .content are editor output and
   are handled with the product description's, in "Rich Text Content" below. */

/* ============================================================
   Rich Text Content (editor-authored)
   ------------------------------------------------------------
   Two blocks on the site render HTML we did not write: the product description
   (.pd-tab-content) and the article body (.article-detail .content). Both come
   from the admin's rich text editor by way of HTMLPurifier, which keeps width,
   height and max-width inside style="". wangEditor writes both dimensions on
   every image it inserts — style="width: 1600px; height: 1200px" — and a pasted
   document brings tables, code blocks and embeds at whatever width they had in
   the source.

   The base `img { max-width: 100%; height: auto }` cannot hold that: an inline
   style outranks a stylesheet declaration, so the width is capped to the card
   while the inline height stands, and a 1600x1200 upload renders 738x1200 —
   near double its correct height. That stretch is the "图片太大了，且错位" the
   report is about.

   Shared section rather than a copy under each page so the two blocks cannot
   drift apart, and placed after both so it is the last word on the elements it
   claims. The only !important declarations in this file are here, on the two
   properties that have to outrank an inline style.
   ============================================================ */
.pd-tab-content,
.article-detail .content {
    /* A pasted URL or an unbroken token wraps instead of widening the card. */
    overflow-wrap: break-word;
}
.pd-tab-content > *:first-child,
.article-detail .content > *:first-child { margin-top: 0; }
.pd-tab-content > *:last-child,
.article-detail .content > *:last-child { margin-bottom: 0; }

/* Images. Three guarantees, and the order they resolve in:

     1. Never distorted. `height: auto` is forced past the editor's inline
        height, so the shape on screen is always the file's own, whatever the
        markup asks for.
     2. Never wider than the card. `max-width: 100%` is forced past any inline
        width or max-width.
     3. Never taller than --rich-media-max-h, so a poster scales down to a size
        that still leaves the rest of the page visible.

   (3) is where object-fit earns its place, and it is not decorative. When the
   width is auto, max-height is a constraint on a replaced element with a known
   ratio and the browser shrinks the whole box proportionally (CSS 2.1 §10.4) —
   measured: a 1600x1200 with no inline width lands at 387x291 against a 291px
   cap. But when the markup *does* carry a width — which is the case we are
   fixing — Chrome keeps that width and clamps the height alone: the same file
   gets a 748x291 box. Without object-fit that box is the stretch all over
   again; with `contain` the picture is drawn inside it at its own ratio.
   It is centred in that box because `text-align: center` is one of the styles
   the editor is allowed to emit, and pinning the picture left would draw a
   deliberately centred image off-centre. A tall portrait reads as centred
   either way, so this is the safer default.

   Author intent to go *smaller* survives: only max-width is forced, never
   width, so an inline `width: 300px` renders at 300px and the height follows
   from it. The half given up is height-driven sizing, which is the half that
   cannot be trusted to agree with the file. */
.pd-tab-content img,
.article-detail .content img {
    max-width: 100% !important;
    height: auto !important;
    max-height: var(--rich-media-max-h);
    object-fit: contain;
    object-position: center;
    border-radius: 4px;
    margin: 10px 0;
    vertical-align: middle;
}

/* A <video> is a replaced element like an image and takes the same treatment.
   An <iframe> has no intrinsic ratio to protect, so it gets the two overflow
   caps only — forcing a height on it would collapse the embed. */
.pd-tab-content video,
.article-detail .content video {
    max-width: 100% !important;
    height: auto !important;
    max-height: var(--rich-media-max-h);
    object-fit: contain;
    object-position: center;
    display: block;
    margin: 10px 0;
}
.pd-tab-content iframe,
.pd-tab-content embed,
.pd-tab-content object,
.article-detail .content iframe,
.article-detail .content embed,
.article-detail .content object {
    max-width: 100% !important;
    max-height: var(--rich-media-max-h);
    border: 0;
    display: block;
    margin: 10px 0;
}
.pd-tab-content figure,
.article-detail .content figure { margin: 12px 0; max-width: 100%; }

/* A pasted table scrolls inside itself. The templates echo the purified HTML as
   one string, so there is no wrapper element to hang an overflow container on;
   display: block makes the table its own scroll container (its rows still lay
   out as a table inside it), and width: max-content up to 100% lets it stay
   narrow when the content is narrow. A twelve-column paste scrolls; the page
   does not widen. */
.pd-tab-content table,
.article-detail .content table {
    display: block;
    width: max-content;
    max-width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
    margin: 0 0 12px;
    font-size: 13px;
}
.pd-tab-content th,
.pd-tab-content td,
.article-detail .content th,
.article-detail .content td {
    border: 1px solid var(--border-color);
    padding: 6px 10px;
    text-align: left;
}
.pd-tab-content th,
.article-detail .content th {
    background: var(--table-header-bg);
    color: var(--text-muted);
    font-weight: 500;
}

/* Code blocks scroll on their own axis for the same reason: a long line is not
   a reason to give the whole page a horizontal scrollbar. */
.pd-tab-content code,
.article-detail .content code {
    background: #f5f5f5;
    padding: 2px 6px;
    border-radius: 3px;
    font-size: 13px;
}
.pd-tab-content pre,
.article-detail .content pre {
    max-width: 100%;
    overflow-x: auto;
    background: #f5f5f5;
    border-radius: var(--radius);
    padding: 12px 14px;
    margin: 0 0 12px;
    font-size: 13px;
    line-height: 1.6;
}
.pd-tab-content pre code,
.article-detail .content pre code {
    background: none;
    padding: 0;
    font-size: inherit;
}

.pd-tab-content blockquote,
.article-detail .content blockquote {
    border-left: 4px solid var(--teal);
    padding: 10px 15px;
    margin: 15px 0;
    background: #f9f9f9;
    color: var(--text-muted);
}

/* ============================================================
   Pagination
   ============================================================ */
.pagination-wrap {
    display: flex;
    justify-content: center;
    margin: var(--block-gap) 0;
}
.pagination-wrap nav { display: flex; gap: 4px; }
.pagination-wrap a,
.pagination-wrap span {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 34px;
    height: 34px;
    padding: 0 8px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    font-size: 13px;
    color: var(--text-muted);
    background: var(--card-bg);
    transition: all .2s;
}
.pagination-wrap a:hover { border-color: var(--teal); color: var(--teal); }
.pagination-wrap .active span {
    background: var(--teal);
    border-color: var(--teal);
    color: #fff;
}
.pagination-wrap .disabled span { opacity: .4; cursor: default; }

/* ============================================================
   Empty State
   ============================================================ */
.empty-state {
    text-align: center;
    padding: 60px 0;
    color: var(--text-light);
}
.empty-state-glyph {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--thumb-icon);
    margin-bottom: 14px;
}
.empty-state-glyph svg {
    width: 56px;
    height: 56px;
}
.empty-state p { margin: 0 0 16px; }

/* ============================================================
   Alerts
   ============================================================ */
.alert {
    padding: 12px 16px;
    border-radius: var(--radius);
    font-size: 14px;
    margin-bottom: 15px;
}
.alert-success { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
.alert-danger { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
.alert-warning { background: #fff3cd; color: #664d03; border: 1px solid #ffecb5; }
.alert-info { background: #cff4fc; color: #055160; border: 1px solid #b6effb; }

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 992px) {
    .site-nav { display: none; }
    .menu-toggle { display: block; }
    .pd-layout { grid-template-columns: 1fr; }
    /* The grid is one column here, so .pd-main must be told to come first;
       without it, order:2 on the sidebar only reorders against a default of 0
       that .pd-main also has, and the pair keeps source order by accident. */
    .pd-main { order: 1; }
    .pd-sidebar { order: 2; }
    .product-grid { grid-template-columns: repeat(3, 1fr); }
    .two-col-section { grid-template-columns: 1fr; }
    :root { --pd-image-size: 132px; }
}

@media (max-width: 768px) {
    .product-table-section { display: none; }
    .product-grid { grid-template-columns: repeat(2, 1fr); }
    .contact-section { flex-direction: column; text-align: center; }
    /* Image and price stay side by side rather than stacking: a full-width
       product shot would push the price and the buy form off the first screen. */
    .pd-head { gap: 14px; }
    :root { --pd-image-size: 116px; }
    .pd-image-wrap { padding: 5px; }
    /* Matches the base selector's specificity (.site-quote.cat-quote), otherwise
       the 14px base gap outranks this rule no matter where it sits in the file. */
    .site-quote.cat-quote { gap: 12px; }
    .cat-quote .cat-thumb-lg { width: 34px; height: 34px; }
    /* The sidebar is below the article now, so the reading column is the full
       width and the article card can hold a page-like measure. */
    .article-detail { padding: 24px; }
}

@media (max-width: 480px) {
    /* One step down for both rhythm tokens: the bands still read as separate,
       without spending a third of a phone screen on white space. */
    :root { --section-gap: 20px; --block-gap: 15px; --pd-image-size: 96px; }

    .product-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
    .pd-info { padding: 15px; }
    .pd-price { font-size: 22px; }
    .article-detail { padding: 20px; }
    .article-detail h1 { font-size: 18px; }
    .pd-head { gap: 12px; margin-bottom: 15px; }
    .pd-image-wrap { padding: 4px; }
    .cat-quote .cat-thumb-lg { width: 30px; height: 30px; }
    .empty-state { padding: 40px 0; }
    /* Card padding was still at its desktop 20-25px on a 375px screen, which is
       ~13% of the width per side. These bring the inner padding in line with
       .pd-info, which already stepped down here. */
    .site-quote { padding: 12px 15px; }
    .pd-tab-content { padding: 15px; }
    .page-card-body { padding: 15px; }
    .page-card-header { padding: 10px 15px; }
    .contact-section { padding: 20px; }
    /* A narrower label column gives every control ~12px more room while the
       five of them stay on the same two edges. */
    .pd-form .form-label { width: 68px; font-size: 13px; }
    /* Track the narrower label so the message stays in the control column. */
    .pd-form .form-error { margin-left: 68px; }
    /* The widget is 300px and cannot shrink; indenting it by another 68px overflows a
       375px screen. Give it the full row instead — measured 399px document width
       against a 375px viewport before this, 375 after. */
    .pd-form .form-turnstile { margin-left: 0; }
    /* Title and date stop competing for one line: the date wrapped under a long
       headline anyway, but as a baseline-aligned flex sibling it landed ragged. */
    .article-page-item {
        flex-direction: column;
        align-items: flex-start;
        gap: 4px;
        padding: 12px 15px;
    }
    .article-detail .article-cover { aspect-ratio: 16 / 9; }
}

/* ----- Desktop table visible only on md+ ----- */
@media (max-width: 768px) {
    .desktop-only { display: none !important; }
}
@media (min-width: 769px) {
    .mobile-only { display: none !important; }
}

/* ----- Utility ----- */
.text-center { text-align: center; }
.text-muted { color: var(--text-light); }
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 15px; }
/* The -3 step is the band separator, so it tracks --section-gap and steps down
   with everything else on a phone instead of staying at its desktop 25px. */
.mt-3 { margin-top: var(--section-gap); }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 15px; }
.mb-3 { margin-bottom: var(--section-gap); }
