/* =============================================================================
   blog.css — page-scoped stylesheet for the public blog index (/blogs)
   -----------------------------------------------------------------------------
   Loaded ONLY by page/blogs/index.php via a <link> after the shared header.
   Everything here is scoped under the root class `.blogidx`, so it cannot leak
   into /help/*, /blog/{slug}, or any other page.

   WHY THIS FILE EXISTS (2026-08-16)
   ---------------------------------
   /blogs used to borrow `css/help.css` and the `.help-center` container. That
   was the root cause of two measured defects:

     1. `css/help.css:690-695` — inside `@media (max-width: 768px)`:
            .help-center svg, .help-card svg, ... { width:36px!important; height:36px!important }
        an `!important` blanket meant for large decorative help-article icons.
        On /blogs it also hit the 18px search icon and the 16px pagination
        chevrons. Measured render at 390px: the search icon came out **12x36**
        and a chevron **6x36** — squashed flat and stretched into a vertical
        sliver — and it inflated the search control from 44px to 60px tall.
        That inflated control is what pushed the page over the viewport at 360px.

     2. `css/help.css:673-687` — `.help-center h2 { font-size:1.5rem!important }`
        / `.help-center h3 { font-size:1.2rem!important }`. Because those are
        `!important` stylesheet rules they beat the page's own inline
        `style="font-size:1.1rem"` on card titles, so a card heading got
        *bigger* on mobile (17.6px desktop -> a 1.2rem/19.2px target at <=768px)
        than it was on desktop. Type that grows as the screen shrinks is exactly
        the "mobile chu qua to" report.

   Neither defect is fixable from inside the page while help.css is loaded —
   `!important` in a media query beats any inline style. Not loading help.css
   at all is the fix; this file replaces the ~6 `.help-center` rules /blogs
   actually used (container width, breadcrumb) with page-scoped equivalents.
   `css/help.css` itself is untouched, so /help/* and /blog/{slug} keep their
   current behaviour.

   TOKENS
   ------
   /blogs is a LIGHT page. It therefore uses the light application palette from
   `css/design-tokens.css` (`--app-*`, `--brand-*`, `--n-*`, `--card-*`), NOT
   the Direction B dark set (`--ink`, `--surface`, `--border`, `--radius-card`,
   `--shadow-card`) — those resolve to near-white ink (#eef0ff) on a dark
   surface (#141726) and would render this page white-on-white. Per
   UI_COMPONENT_LIBRARY.md §1.1 a page is fully one system or fully the other,
   never mixed. `--a1`/`--a2` ARE used indirectly: `--brand-400`/`--cyan-400`
   are defined as the same values and are the documented anchor points between
   the light and dark systems. No new hex value is introduced anywhere below.

   TYPE SCALE
   ----------
   One fluid modular scale (`clamp()`), declared once at `.blogidx` and consumed
   by every text role. Fluid interpolation removes the need for per-breakpoint
   `font-size: ... !important` overrides entirely — which is what the old page
   had 22 of, and what made the mobile sizes non-monotonic. Ratio is a minor
   third (1.2) at 360px widening to 1.25 at 1440px.
   ============================================================================= */

.blogidx {
    /* --- fluid type scale (min @360px  ->  max @1440px) --- */
    --bx-fs-h1:      clamp(1.5rem, 1.15rem + 1.1vw, 2rem);        /* 24 -> 32 */
    --bx-fs-hero:    clamp(1.25rem, 0.98rem + 1.15vw, 1.75rem);   /* 20 -> 28 */
    --bx-fs-section: clamp(1.25rem, 1.05rem + 0.6vw, 1.5rem);     /* 20 -> 24 */
    --bx-fs-card:    clamp(1rem, 0.94rem + 0.25vw, 1.125rem);     /* 16 -> 18 */
    --bx-fs-body:    1rem;                                        /* 16       */
    --bx-fs-sm:      0.875rem;                                    /* 14       */
    --bx-fs-xs:      0.8125rem;                                   /* 13       */

    /* --- line heights: tight for display, open for prose --- */
    --bx-lh-tight: 1.25;
    --bx-lh-snug:  1.35;
    --bx-lh-body:  1.6;

    /* --- vertical rhythm: one step, used everywhere (no ad-hoc 60px/40px/25px) --- */
    --bx-gap:      clamp(16px, 1.1vw + 12px, 24px);
    --bx-section:  clamp(32px, 3vw, 56px);

    /* --- minimum comfortable hit area (WCAG 2.5.8 / Apple HIG) --- */
    --bx-tap: 44px;

    max-width: 1200px;      /* ~72ch of body text in the widest single column */
    margin: 0 auto;
    padding: clamp(16px, 2vw, 28px) clamp(12px, 2vw, 20px) var(--bx-section);
    color: var(--app-ink, #16202f);
    font-size: var(--bx-fs-body);
    line-height: var(--bx-lh-body);
}

/* Belt-and-braces against the classic flexbox/grid overflow: every direct
   descendant is allowed to shrink below its intrinsic content width. This is
   the single most common cause of page-level horizontal scroll and it is
   cheaper to state once than to chase per component. */
.blogidx *,
.blogidx *::before,
.blogidx *::after {
    min-width: 0;
}

.blogidx a {
    color: inherit;
    text-decoration: none;
}

.blogidx :focus-visible {
    outline: 2px solid var(--brand-600, #2158d8);
    outline-offset: 2px;
    border-radius: 4px;
}

/* Visually hidden but announced. `.sr-only` exists in css/landing.css, which
   this page does not load — scoped copy rather than a cross-page dependency. */
.blogidx .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* =============================================================================
   1. Page header + breadcrumb
   ============================================================================= */

.blogidx-crumbs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-bottom: 12px;
    font-size: var(--bx-fs-sm);
    color: var(--app-ink-soft, #667487);
}

.blogidx-crumbs a,
.blogidx-crumbs > span {
    display: inline-flex;
    align-items: center;
    /* Breadcrumb links are the one control on this page under the 44px target.
       They use the project's own canonical compact-control height instead
       (--ctl-h, design-tokens.css) — a 44px breadcrumb would be taller than
       the page title's own leading. Every other interactive element on the
       page (chips, search, pagination, cards, CTA) is >= 44px. */
    min-height: var(--ctl-h, 34px);
}

.blogidx-crumbs a {
    color: var(--brand-600, #2158d8);
}

.blogidx-crumbs a:hover {
    text-decoration: underline;
}

.blogidx-crumbs-sep {
    color: var(--app-ink-faint, #95a0b5);
}

.blogidx-head {
    /* --bx-gap, not --bx-section: the head + breadcrumb pushed the first post
       card to y=476 at 1440x900, over UX_PAGE_STANDARD §1's 450 ceiling. The
       head already separates itself with a rule + padding, so the extra 32px
       of margin was buying nothing. */
    margin-bottom: var(--bx-gap);
    padding-bottom: var(--bx-gap);
    border-bottom: 1px solid var(--app-border, #dfe5ef);
}

.blogidx-head h1 {
    margin: 0 0 8px;
    font-size: var(--bx-fs-h1);
    line-height: var(--bx-lh-tight);
    font-weight: 700;
    color: var(--app-ink, #16202f);
    letter-spacing: -0.01em;
}

.blogidx-head-sub {
    margin: 0;
    /* 58ch, not 68: `ch` is the width of "0", and Vietnamese lowercase glyphs
       are narrower than that, so a 68ch box measured 82 real characters per
       line — over the 45-75 comfortable measure. 58ch measures ~70. */
    max-width: 58ch;
    font-size: var(--bx-fs-body);
    line-height: var(--bx-lh-body);
    color: var(--app-ink-soft, #667487);
}

/* =============================================================================
   2. Section shell — one heading treatment for every band on the page
   ============================================================================= */

.blogidx-section {
    margin-bottom: var(--bx-section);
}

.blogidx-section:last-child {
    margin-bottom: 0;
}

.blogidx-section-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px var(--bx-gap);
    margin-bottom: var(--bx-gap);
}

.blogidx-section-title {
    margin: 0;
    font-size: var(--bx-fs-section);
    line-height: var(--bx-lh-tight);
    font-weight: 700;
    color: var(--app-ink, #16202f);
}

.blogidx-section-note {
    font-size: var(--bx-fs-sm);
    color: var(--app-ink-soft, #667487);
}

/* =============================================================================
   3. Thumbnail frame — shared by every card size
   -----------------------------------------------------------------------------
   `aspect-ratio` reserves the box before the image loads, so nothing reflows
   (no CLS) and a missing file degrades to a flat muted panel with a centred
   glyph instead of a browser broken-image icon plus collapsed height.
   ============================================================================= */

.blogidx-thumb {
    position: relative;
    display: block;
    overflow: hidden;
    background: var(--app-surface-muted, #eaeff7);
    color: var(--app-ink-faint, #95a0b5);
    aspect-ratio: 16 / 9;
}

.blogidx-thumb img {
    width: 100%;
    height: 100%;
    /* `contain`, not `cover` (2026-08-17). Every one of the 33 posts in
       blog_posts has an uploaded featured_image, and the upload path
       (page/admin-blog-add.php, page/admin-blog-edit.php) is a bare
       move_uploaded_file() — no resize/crop/aspect-ratio step exists anywhere
       in it, confirmed by grep (no imagecreate/getimagesize/resize call). So
       the source ratio is whatever the uploader's file happened to be, and the
       content itself is AI-generated infographics with a logo + on-image text
       (not photography), so ANY crop can slice through a word or the logo —
       that is exactly the reported bug (lead image cut through the "NS Ads
       Xperts" wordmark). `contain` is the only option that cannot ever cut
       content regardless of source ratio; the cost is visible letterbox
       margin inside the frame, which the muted panel background below makes
       read as intentional rather than broken. */
    object-fit: contain;
    display: block;
    /* Hides the browser's broken-image glyph + alt text when the file is
       missing (uploads/ is gitignored locally); the muted panel below shows
       through instead. Production, where the files exist, is unaffected. */
    color: transparent;
    /* Paints the image ABOVE .blogidx-thumb-fallback. Not cosmetic — without
       it the fallback icon sits on top of every successfully-loaded thumbnail.
       page/blogs/index.php:173 emits the fallback <span> unconditionally, before
       the <img>, and that span is position:absolute while the <img> was static;
       CSS paint order puts positioned elements above non-positioned ones no
       matter the DOM order, so the grey file-text glyph rendered over the middle
       of every real image. It stayed invisible for as long as local uploads/ was
       empty (every <img> 404'd, so there was nothing behind the icon to occlude)
       and only surfaced once this task's verification generated real test files.
       position:relative is enough — both are then positioned, and DOM order
       decides, which puts the later <img> on top. The fallback still shows for a
       missing file, because a broken <img> paints nothing (see color:transparent
       above) and the muted panel plus icon show through it. */
    position: relative;
}

.blogidx-thumb-fallback {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Category chip rides on the thumbnail rather than in the meta row.
   Reason is measured, not stylistic: in the meta row it pushed the row to a
   second line on some cards and not others (Vietnamese category names are
   long), so cards in the same row ended with ragged internal spacing. On the
   thumbnail it is out of the text flow entirely and the meta row is
   guaranteed one line at every viewport down to 360px. */
.blogidx-thumb .blogidx-tag {
    position: absolute;
    left: 10px;
    bottom: 10px;
    max-width: calc(100% - 20px);
    background: var(--app-surface, #fff);
    box-shadow: var(--card-shadow, 0 1px 2px rgba(27, 32, 56, .06));
}

/* =============================================================================
   4. Hero band — 1 lead post + 2 runners-up, equal height by construction
   ============================================================================= */

.blogidx-hero {
    display: grid;
    grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
    gap: var(--bx-gap);
}

.blogidx-card {
    display: flex;
    flex-direction: column;
    background: var(--app-surface, #fff);
    border: var(--card-border, 1px solid #dfe5ef);
    border-radius: var(--card-radius, 10px);
    box-shadow: var(--card-shadow, 0 1px 2px rgba(27, 32, 56, .06));
    overflow: hidden;
    transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease;
}

.blogidx-card:hover,
.blogidx-card:focus-within {
    box-shadow: var(--card-shadow-hover, 0 4px 12px rgba(27, 32, 56, .09));
    border-color: var(--app-border-strong, #c9d2e0);
    transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
    .blogidx-card {
        transition: none;
    }

    .blogidx-card:hover,
    .blogidx-card:focus-within {
        transform: none;
    }
}

.blogidx-card-body {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: var(--bx-gap);
    flex: 1 1 auto;
}

/* --- lead post ---
   No rule for `.blogidx-hero-lead .blogidx-thumb` at this width on purpose:
   below 768px it fully inherits `aspect-ratio:16/9` from `.blogidx-thumb`
   above and that alone is enough (see the >=768px override below for why).
   There used to be a `max-height:300px` here, added to stop a 780px-wide
   column from producing a 440px-tall image that ate the first screen
   (UX_PAGE_STANDARD §1 — the old page put the lead title at y=593). The
   *goal* was right, the mechanism was the bug: `max-height` and the
   inherited `aspect-ratio:16/9` are two independent constraints on the same
   box, and at wide columns they disagreed — `aspect-ratio` wanted the box
   taller than 300px, so `max-height` clamped it, and then `object-fit:cover`
   filled the clamped box by cropping the source image a second time. That
   second crop is what cut through the "NS Ads Xperts" wordmark in the
   reported bug. Fixed below by replacing both with one ratio at once. */

@media (min-width: 768px) {
    .blogidx-hero-lead .blogidx-thumb {
        /* One constraint, not two: this ratio REPLACES the inherited 16:9 at
           this width, it does not sit alongside a max-height like before —
           see the comment on the (now ratio-less) base rule above.
           21/9 (~2.33) is not an arbitrary "flatter" guess: measured with
           Playwright against live /blogs (uploads/ is empty locally, so this
           was checked against layout, not a real photo), the lead column is
           697.1px wide at every viewport >=1200px (the container caps at
           max-width:1200px, so 1440px and 1920px measure identically). 16:9
           at that width wants 392px tall (too much — the y=593 problem this
           block exists to prevent); 21/9 gives 697.1 / (21/9) = 298.8px,
           within 1px of the ~300px the old max-height targeted, with no
           second constraint left to fight it. Below 768px the column is
           already narrow enough (<=533px at the 767px edge, where 16:9 alone
           already yields <=300px) that no override is needed there — the
           base rule above is intentionally empty. */
        aspect-ratio: 21 / 9;
    }
}

.blogidx-hero-title {
    margin: 0;
    font-size: var(--bx-fs-hero);
    line-height: var(--bx-lh-tight);
    font-weight: 700;
    letter-spacing: -0.01em;
    overflow-wrap: anywhere;
}

.blogidx-hero-excerpt {
    margin: 0;
    max-width: 58ch;                 /* see the note on .blogidx-head-sub */
    font-size: var(--bx-fs-body);
    line-height: var(--bx-lh-body);
    color: var(--app-ink-soft, #667487);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* --- runners-up column: two cards that together match the lead's height --- */
.blogidx-hero-side {
    display: flex;
    flex-direction: column;
    gap: var(--bx-gap);
}

.blogidx-hero-side .blogidx-card {
    flex: 1 1 0;
}

.blogidx-hero-side .blogidx-thumb {
    /* Fixed 16:9, not a flex-derived height (2026-08-17). This used to be
       `flex:1 1 auto; min-height:96px; aspect-ratio:auto` — the thumb's
       height came from its share of `.blogidx-hero-side`'s flex column, which
       varies with how many lines each card's title wraps to. That is a crop
       ratio nobody chose: two identical images ended up different heights
       (hence cropped differently) purely because the neighbouring card's
       title happened to be one word longer. Height cannot come from content
       length AND stay a controlled ratio at the same time, so it now comes
       from `aspect-ratio` like every other thumb on the page. Height parity
       with the lead card (the reason the flex approach existed) still holds:
       `.blogidx-card{flex:1 1 0}` already equalises the two cards' outer
       height, and `.blogidx-card-body{flex:1 1 auto}` +
       `.blogidx-meta{margin-top:auto}` (section 5) already absorb any leftover
       space by pinning the meta row down — nothing new needed for that.
       Value matches the existing <768px override one screen down exactly
       (16/9, flex:0 0 auto), which is what makes that override dead code —
       removed in the media query below instead of kept as a duplicate. */
    aspect-ratio: 16 / 9;
    flex: 0 0 auto;
}

.blogidx-mini-title {
    margin: 0;
    font-size: var(--bx-fs-card);
    line-height: var(--bx-lh-snug);
    font-weight: 600;
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* =============================================================================
   5. Meta line — date, read time, category. One row, never wraps to 3 lines.
   -----------------------------------------------------------------------------
   Author was removed on purpose: 14 of 15 cards said the same name, so it cost
   a line of every card and carried no information. It stays on the detail page,
   where it identifies one specific post's writer.
   ============================================================================= */

.blogidx-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
    margin-top: auto;               /* pins meta to the card's bottom edge */
    padding-top: 4px;
    font-size: var(--bx-fs-sm);
    line-height: var(--bx-lh-snug);
    color: var(--app-ink-soft, #667487);
}

.blogidx-meta-item {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    white-space: nowrap;
}

.blogidx-meta svg {
    /* explicit, so no ancestor blanket rule can ever restyle these again */
    width: 14px;
    height: 14px;
    flex: 0 0 auto;
    color: var(--app-ink-faint, #95a0b5);
}

/* Category as a quiet chip inside the meta row rather than a loud all-caps
   eyebrow above the title. It is a secondary attribute of the post, so per
   UX_PAGE_STANDARD §3 it belongs under the primary identifier in smaller,
   dimmer type — not competing with it. */
.blogidx-tag {
    display: inline-flex;
    align-items: center;
    max-width: 100%;
    padding: 2px 8px;
    border-radius: var(--radius-pill, 8px);
    background: var(--app-primary-soft, #eff5ff);
    color: var(--brand-700, #1a45ab);
    font-size: var(--bx-fs-xs);
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* =============================================================================
   6. Toolbar — category filter + search, together, directly above the list
   -----------------------------------------------------------------------------
   Previously the category chips and the search box were separate blocks about
   700px apart with two card grids between them. Both narrow the same list, so
   they belong in the same control group, adjacent to what they control.
   ============================================================================= */

.blogidx-toolbar {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: var(--bx-gap);
}

.blogidx-search {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    width: 100%;
    max-width: 520px;
}

.blogidx-search-field {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 200px;
    min-width: 0;                   /* the actual fix for the 360px overflow:
                                       a flex item's default min-width:auto is
                                       the input's intrinsic size (~229px), so
                                       input + gap + button could not shrink
                                       under 380px inside a 350px container. */
}

.blogidx-search-field svg {
    position: absolute;
    left: 12px;
    width: 16px;
    height: 16px;
    pointer-events: none;
    color: var(--app-ink-faint, #95a0b5);
}

.blogidx-search input {
    width: 100%;
    min-width: 0;
    min-height: var(--bx-tap);
    padding: 10px 12px 10px 36px;
    border: 1px solid var(--app-border-strong, #c9d2e0);
    border-radius: var(--radius-pill, 8px);
    background: var(--app-surface, #fff);
    color: var(--app-ink, #16202f);
    font-size: var(--bx-fs-body);
    font-family: inherit;
}

.blogidx-search input::placeholder {
    color: var(--app-ink-faint, #95a0b5);
}

.blogidx-search input:focus {
    outline: none;
    border-color: var(--brand-600, #2158d8);
    box-shadow: var(--focus-ring, 0 0 0 3px rgba(33, 88, 216, .28));
}

.blogidx-search button {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: var(--bx-tap);
    padding: 10px 18px;
    border: none;
    border-radius: var(--radius-pill, 8px);
    background: var(--brand-gradient, linear-gradient(135deg, #2158d8 0%, #0e7490 100%));
    color: #fff;
    font-size: var(--bx-fs-sm);
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
}

.blogidx-search button svg {
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
}

.blogidx-search button:hover {
    box-shadow: var(--shadow-primary-hover, 0 6px 16px rgba(33, 88, 216, .34));
}

/* --- category chips ---
   Desktop: wrap (never scroll sideways). Mobile (<768px) is the opposite —
   one horizontally-scrolling row, see the `@media (max-width:767px)` block
   in section 11 for why and how. This file has exactly one breakpoint, so
   "wrap" here is really "wrap above 767px"; the comment used to just say
   "wrap, never scroll sideways" with no width qualifier, which stopped being
   true the moment section 11 added the scroll-row override below it. */
.blogidx-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.blogidx-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: var(--bx-tap);
    padding: 8px 14px;
    border: 1px solid var(--app-border, #dfe5ef);
    border-radius: var(--radius-pill, 8px);
    background: var(--app-surface, #fff);
    color: var(--app-ink, #16202f);
    font-size: var(--bx-fs-sm);
    font-weight: 500;
    line-height: 1.3;
}

.blogidx-chip:hover {
    border-color: var(--brand-600, #2158d8);
    background: var(--app-primary-soft, #eff5ff);
}

.blogidx-chip[aria-current="true"] {
    border-color: var(--brand-600, #2158d8);
    background: var(--app-primary-soft, #eff5ff);
    color: var(--brand-700, #1a45ab);
    font-weight: 600;
}

.blogidx-chip-count {
    color: var(--app-ink-faint, #95a0b5);
    font-size: var(--bx-fs-xs);
    font-variant-numeric: tabular-nums;
}

.blogidx-chip[aria-current="true"] .blogidx-chip-count {
    color: var(--brand-600, #2158d8);
}

/* =============================================================================
   7. Post grid — responsive with no breakpoints and no possible overflow
   -----------------------------------------------------------------------------
   `minmax(min(100%, 300px), 1fr)` is the key: the `min(100%, 300px)` floor lets
   a track fall below 300px when the container itself is narrower, which is what
   stops a fixed minmax floor from forcing horizontal scroll on small phones.
   ============================================================================= */

.blogidx-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
    gap: var(--bx-gap);
}

.blogidx-card-title {
    margin: 0;
    font-size: var(--bx-fs-card);
    line-height: var(--bx-lh-snug);
    font-weight: 600;
    color: var(--app-ink, #16202f);
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* Reserve exactly two lines so every card in a row starts its excerpt on
       the same baseline. Same technique as `.capability-card h3` in
       css/landing.css — 2 * font-size * line-height. */
    min-height: calc(2em * var(--bx-lh-snug));
}

.blogidx-card-excerpt {
    margin: 0;
    font-size: var(--bx-fs-sm);
    line-height: var(--bx-lh-body);
    color: var(--app-ink-soft, #667487);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Whole card is the target, but the <a> around the title is the accessible
   name and the visible link (UX_PAGE_STANDARD §4: the primary identifier is
   what must be clickable, not a buried "read more"). */
.blogidx-card-link::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
}

.blogidx-card {
    position: relative;
}

/* =============================================================================
   8. "Most read" — a ranked text list, deliberately NOT another card grid
   -----------------------------------------------------------------------------
   The old page rendered this block and the main list with byte-identical card
   markup, one after the other. Two identical 6-card grids read as one 12-card
   grid that inexplicably restarts. A dense numbered list is visually distinct,
   fits 6 items in the height 2 cards used to take, and — by showing the view
   count the ranking is actually based on — explains why the order is what it is.
   ============================================================================= */

.blogidx-ranklist {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
    gap: 0 var(--bx-section);
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: bx-rank;
    border-top: 1px solid var(--app-border, #dfe5ef);
}

/* Every item carries the same bottom rule, and the list carries a top rule.
   A `:last-child` exception was tried first and is wrong here: with
   `auto-fill` the item that is last in source order is not the last item of
   every column, so at 3 columns two columns ended with a hairline and one
   did not — a visibly ragged bottom edge. Uniform rules read correctly at
   1, 2 or 3 columns without knowing the count. */
.blogidx-rank {
    counter-increment: bx-rank;
    border-bottom: 1px solid var(--app-border, #dfe5ef);
}

.blogidx-rank > a {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    gap: 12px;
    min-height: var(--bx-tap);
    padding: 12px 4px;
}

.blogidx-rank > a::before {
    content: counter(bx-rank, decimal-leading-zero);
    font-size: var(--bx-fs-body);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--app-ink-faint, #95a0b5);
}

.blogidx-rank > a:hover .blogidx-rank-title {
    color: var(--brand-600, #2158d8);
}

.blogidx-rank-title {
    display: block;
    font-size: var(--bx-fs-body);
    line-height: var(--bx-lh-snug);
    font-weight: 600;
    color: var(--app-ink, #16202f);
    overflow-wrap: anywhere;
}

.blogidx-rank .blogidx-meta {
    margin-top: 4px;
    padding-top: 0;
}

/* =============================================================================
   9. Results summary / empty state / pagination
   ============================================================================= */

.blogidx-summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 12px;
    margin-bottom: var(--bx-gap);
    font-size: var(--bx-fs-sm);
    color: var(--app-ink-soft, #667487);
}

.blogidx-summary a {
    color: var(--brand-600, #2158d8);
    font-weight: 600;
}

.blogidx-summary a:hover {
    text-decoration: underline;
}

.blogidx-empty {
    padding: var(--bx-section) var(--bx-gap);
    border: 1px dashed var(--app-border-strong, #c9d2e0);
    border-radius: var(--card-radius, 10px);
    background: var(--app-surface, #fff);
    text-align: center;
    color: var(--app-ink-soft, #667487);
}

.blogidx-empty p {
    margin: 0 0 4px;
}

/* Reuse the canonical public pagination atom (.pagination / .page-link,
   css/style.css:1316-1345 — UI_COMPONENT_LIBRARY §2.7). Only the hit area is
   adjusted here, page-scoped: the shared `.page-link` is 8px/14px padding at
   14px type, which measures ~37px tall — under the 44px minimum. */
.blogidx .pagination {
    margin-top: var(--bx-section);
    align-items: center;
    gap: 6px;
}

.blogidx .page-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-width: var(--bx-tap);
    min-height: var(--bx-tap);
    font-variant-numeric: tabular-nums;
}

.blogidx .page-link svg {
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
}

.blogidx-pagination-status {
    width: 100%;
    margin-top: 10px;
    text-align: center;
    font-size: var(--bx-fs-sm);
    color: var(--app-ink-soft, #667487);
    font-variant-numeric: tabular-nums;
}

/* =============================================================================
   10. Closing CTA
   ============================================================================= */

.blogidx-cta {
    padding: var(--bx-section) var(--bx-gap);
    border: var(--card-border, 1px solid #dfe5ef);
    border-radius: var(--card-radius, 10px);
    background: var(--app-surface-muted, #eaeff7);
    text-align: center;
}

.blogidx-cta h2 {
    margin: 0 auto 10px;
    /* Wide enough that the Vietnamese title fits one line at >=1024px. It used
       to be capped narrow, which forced a 2-line wrap that landed between
       "pha" and "huy" — one Vietnamese word split across lines. Vietnamese
       words are multi-syllable with real spaces, so the browser treats every
       syllable boundary as a break opportunity; the only reliable remedy is
       not forcing a wrap in the first place. */
    max-width: 56ch;
    font-size: var(--bx-fs-section);
    line-height: var(--bx-lh-tight);
    font-weight: 700;
    color: var(--app-ink, #16202f);
    /* Without this a hard 2-line wrap fell between "phá" and "hủy", splitting
       one Vietnamese word across lines. `balance` evens the line lengths and
       degrades to normal wrapping where unsupported. */
    text-wrap: balance;
}

.blogidx-cta p {
    margin: 0 auto var(--bx-gap);
    max-width: 72ch;               /* same reason as the h2 above */
    font-size: var(--bx-fs-body);
    line-height: var(--bx-lh-body);
    color: var(--app-ink-soft, #667487);
}

.blogidx-cta-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: var(--bx-tap);
    padding: 12px 28px;
    border-radius: var(--radius-pill, 8px);
    background: var(--brand-gradient, linear-gradient(135deg, #2158d8 0%, #0e7490 100%));
    color: #fff;
    font-size: var(--bx-fs-body);
    font-weight: 600;
    box-shadow: var(--shadow-primary, 0 2px 6px rgba(33, 88, 216, .28));
}

.blogidx-cta-btn:hover {
    box-shadow: var(--shadow-primary-hover, 0 6px 16px rgba(33, 88, 216, .34));
}

/* =============================================================================
   11. Narrow viewports
   -----------------------------------------------------------------------------
   Still the only breakpoint in the file (its >=768px complement lives next to
   `.blogidx-hero-lead .blogidx-thumb` in section 4, not here — kept beside the
   rule it overrides rather than filed by direction). Covers the hero's
   2-column split AND, since 2026-08-17, the category chip rail below. There is
   deliberately no `font-size` override anywhere below; the clamp() scale
   already covers 360px to 1440px.
   ============================================================================= */

@media (max-width: 767px) {
    .blogidx-hero {
        grid-template-columns: minmax(0, 1fr);
    }

    /* `.blogidx-hero-side .blogidx-thumb` and `.blogidx-hero-lead .blogidx-thumb`
       used to have overrides here (16:9 + flex:0 0 auto on the former,
       `max-height:none` reset on the latter). Both are gone: the base rules in
       section 4 now set those exact values directly (hero-side) or set no
       max-height to reset in the first place (hero-lead), so the value here
       would have been byte-identical to the unconditional base rule — a copy
       that could silently drift out of sync with it, not a real override. */

    .blogidx-search {
        max-width: none;
    }

    /* --- category chip rail: one scrolling row instead of wrap -----------
       6 real chuyên mục (5 categories + "Tất cả chuyên mục", from
       blog_categories in the local DB) at Vietnamese-length names wrap to 3
       ragged rows in a 350px-wide content column — the reported "tag bị rớt
       hàng" complaint. Switched to a single scrolling row using the two
       scroll-rail techniques that already exist elsewhere in this codebase,
       rather than inventing a third:
         - overflow-x/-y + -webkit-overflow-scrolling + scrollbar-width: same
           four declarations as `.dashboard-stats` (css/style.css:2748-2755).
         - scroll-snap-type:x proximity + scroll-snap-align:start: same as
           `.page-tabs`/`.page-tab` (css/page-tabs.css:39-46). `proximity`,
           not `mandatory` like `.hscroll-track` in landing.css — this is a
           secondary filter a visitor may only be skimming past, not a
           carousel they're meant to stop on each item of; `mandatory` would
           fight a fast flick scroll back to a dead stop on every chip. */
    .blogidx-chips {
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: thin;
        scroll-snap-type: x proximity;
        /* Right-edge fade instead of a static "vuốt để xem thêm" caption
           (like `.hscroll-hint` in landing.css): a caption line is either
           always shown (extra line on every load, even the rare case where
           6 chips already fit without overflow) or has to know when content
           overflows to hide itself, which needs the same JS this page
           doesn't otherwise need. `mask-image`
           fades by the element's own alpha, so it needs no such check and no
           dark-token dependency (`.hscroll-hint` lives on Direction B's dark
           tokens; this page is light-only, see the file header).
           Known limitation, accepted for v1: this fade is static — it never
           turns off once the rail is scrolled to its end, and there's no
           matching left-edge fade once scrolled away from the start. Doing
           that correctly needs a `scroll` listener toggling the mask, which
           is more machinery than a first pass warrants; noted as a possible
           follow-up rather than solved here. */
        -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
        mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
    }

    /* Target is `.blogidx-chips > li`, NOT `.blogidx-chip` (2026-08-17, real
       bug found while testing this task with Playwright, not in the original
       plan). `.blogidx-chips` is a `<ul>`; `display:flex` on it makes its
       `<li>` children the flex items — `.blogidx-chip` is the class on the
       `<a>` *inside* each `<li>` (page/blogs/index.php's toolbar markup), one
       level too deep to be a flex item at all. `flex`/`scroll-snap-align`
       declared on `.blogidx-chip` were therefore inert: measured with
       Playwright, chip text was wrapping into 3-4 stacked lines per pill
       instead of staying one line, because the un-targeted `<li>` kept its
       flex-shrink:1 default and got compressed toward its wrapped
       min-content width. Moving both properties to the actual flex item
       fixes it — confirmed by the same measurement afterwards (single-line
       chips, `scrollWidth` unchanged at 392px for the 6 real categories). */
    .blogidx-chips > li {
        /* Without this, `flex-wrap:nowrap` above still lets a flex item
           shrink below its content size by default (`flex-shrink:1`), so
           long chip labels would compress into wrapped, uneven-height pills
           instead of the row simply overflowing into the scroll it now has.
           Desktop doesn't need this: `wrap` never shrinks items, it moves
           them to the next line instead. `scroll-snap-align:start` pairs
           with `scroll-snap-type:x proximity` above so a flick settles with a
           whole chip at the left edge, never cut mid-label. */
        flex: 0 0 auto;
        scroll-snap-align: start;
    }
}
