/* hook test */
/* Grid Mode Styles
 * Browse/discovery view showing multiple flashcards in a grid
 * Desktop-only: Multi-column grid with click-to-focus
 * Updated: 2026-02-25 (hook test)
 *
 * Premium Polish Features:
 * - Staggered card entrance animations
 * - Enhanced hover states with depth
 * - Smooth 3D flip with perspective
 * - Micro-interactions on buttons
 * - Animated filter chips
 */

/* =============================================================================
   CSS CUSTOM PROPERTIES (Design Tokens)
   ============================================================================= */

:root {
    /* Card colors */
    --grid-card-bg: rgba(250, 250, 247, 0.94);
    --grid-card-border: rgba(44, 44, 44, 0.04);
    --grid-card-shadow: 0 1px 3px rgba(44, 44, 44, 0.04), 0 4px 12px rgba(44, 44, 44, 0.03);
    --grid-card-shadow-hover: 0 1px 3px rgba(44, 44, 44, 0.04), 0 4px 16px rgba(61, 90, 128, 0.06);

    /* Text colors */
    --grid-japanese-color: #332F2B;  /* Warm sumi ink */
    --grid-romanization-color: #4A4642;

    /* Button colors */
    --grid-audio-btn: rgba(61, 90, 128, 0.85);
    --grid-audio-btn-hover: rgba(61, 90, 128, 1);
    --grid-focus-btn: rgba(61, 90, 128, 0.85);
    --grid-focus-btn-hover: rgba(61, 90, 128, 1);
    --grid-known-btn: rgba(163, 177, 138, 0.85);
    --grid-learning-btn: rgba(194, 130, 114, 0.85);

    /* Animation timing */
    --grid-transition-fast: 0.15s;
    --grid-transition-medium: 0.25s;
    --grid-transition-slow: 0.4s;
    --grid-flip-duration: 0.45s;

    /* Easing functions */
    --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
    --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

/* =============================================================================
   MODE VISIBILITY
   ============================================================================= */

/* Hide flashcard container and related elements in grid mode */
/* NOTE: Must match specificity of standard-mode.css selector:
   body:not(.focus-mode):not(.video-mode) .flashcard-container { display: flex !important; }
   Adding :not() pseudo-classes to match specificity (0,0,4,1) */
body.grid-mode:not(.focus-mode):not(.video-mode) .flashcard-container,
body.grid-mode:not(.focus-mode):not(.video-mode) .premium-progress-wrapper {
    display: none !important;
}

/* Also hide the single card view elements */
body.grid-mode .flashcard {
    display: none !important;
}

/* Hide focus-only elements in grid mode */
body.grid-mode .focus-exit-button,
body.grid-mode .japanese-flag,
body.grid-mode .lower-third,
body.grid-mode .lesson-badge,
body.grid-mode .mobile-exit-group,
body.grid-mode #card-number {
    display: none !important;
}

/* =============================================================================
   GRID CONTAINER - FLEX LAYOUT INTEGRATION
   ============================================================================= */

/* Grid container participates in the flex layout hierarchy */
.grid-container {
    display: none;  /* Hidden by default, shown in grid mode */
    flex: 1;        /* Take available space like flashcard-container */
    flex-direction: column;
    width: 100%;
    max-width: 1800px;    /* Wider on desktop for more cards */
    margin: 0 auto;
    margin-top: var(--top-bar-height, 90px);  /* Account for fixed top bar */
    padding: 12px 40px 20px;   /* Reduced top padding for balanced spacing */
    overflow-y: auto;     /* Scroll within the flex item */
    box-sizing: border-box;
    /* Establish stacking context below nav bar (z-index: 98) */
    position: relative;
    z-index: 1;
    isolation: isolate;  /* Contain 3D transformed cards within this context */
    /* Smooth push when sidebars open */
    transition: margin-left 250ms cubic-bezier(0.16, 1, 0.3, 1),
                margin-right 250ms cubic-bezier(0.16, 1, 0.3, 1),
                max-width 250ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* Push grid right when browse sidebar is open (sidebar flush at left:0) */
body.browse-sidebar-open .grid-container {
    margin-left: var(--sidebar-w, 260px);
    max-width: min(1800px, calc(100% - var(--sidebar-w, 260px)));
}

/* Below 1440px (laptops), tighten side padding so the 380px column min
   reaches 3 columns instead of leaving cards two-at-a-time */
@media (min-width: 769px) and (max-width: 1440px) {
    .grid-container {
        padding-left: 24px;
        padding-right: 24px;
    }
}


/* When grid mode is active, show grid container with flex display.
   margin-top is driven by --nav-bar-total-height (set by ResizeObserver in JS,
   consumed in ink-wash-tabs.css) — no hardcoded values here. */
body.grid-mode .grid-container {
    display: flex;
}

/* Keep grid layout tree warm during focus mode to avoid 200ms+ reflow
   when returning. visibility:hidden preserves layout; display:none destroys it. */
.grid-container.grid-warm {
    display: flex !important;
    visibility: hidden;
    position: absolute;
    pointer-events: none;
    height: 0;
    overflow: hidden;
}
body.grid-mode .grid-container.grid-warm {
    visibility: visible;
    position: relative;
    pointer-events: auto;
    height: auto;
    overflow-y: auto;
}

/* Header icon buttons — warm neutral, deferential (落款 seal-stamp quiet) */
body.grid-mode .icon-btn {
    width: 40px;
    height: 40px;
    background: rgba(44, 44, 44, 0.03);
    border: none;
    color: var(--color-romaji);
    font-size: 16px;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

body.grid-mode .icon-btn:hover {
    background: rgba(44, 44, 44, 0.06);
    color: var(--color-stone);
    transform: none;
}

body.grid-mode .icon-btn:active {
    background: rgba(44, 44, 44, 0.08);
    transform: scale(0.96);
}

/* Segment buttons (shared by drawers + settings; the frosted
   .nav-segmented-control wrapper was retired as dead chrome) */
.segment-btn {
    display: flex;
    align-items: center;
    gap: 5px;
    height: 32px;
    padding: 0 14px;
    border-radius: 18px;
    border: none;
    background: transparent;
    color: var(--color-stone);
    font-family: 'Inter', sans-serif;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition:
        all var(--grid-transition-fast) ease,
        transform var(--grid-transition-fast) var(--ease-out-back);
}

.segment-btn:hover:not(.active) {
    background: rgba(255, 255, 255, 0.6);
    color: var(--color-ink);
}

.segment-btn.active {
    background: var(--color-card);
    color: var(--color-ink);
    box-shadow:
        0 2px 8px rgba(44, 44, 44, 0.06),
        0 1px 2px rgba(44, 44, 44, 0.04);
    transform: scale(1.02);
}

.segment-btn .segment-icon {
    font-size: 13px;
    font-weight: 700;
}

/* =============================================================================
   CARD GRID - DESKTOP
   ============================================================================= */

/* The container takes the boot landing focus (tabindex=-1, never tab-reachable),
   so the UA's blue ring must never paint around the whole grid. */
.grid-cards:focus,
.grid-cards:focus-visible {
    outline: none;
}

.grid-cards {
    display: grid;
    /* 400px min = the pre-refactor density (3 columns max, matching masonry).
       The 380px experiment (wave-1 1366-laptop fix) let a 4th column sneak in
       on wide windows under the 1800px container cap — too dense; owner round 3
       restored 400px. The 1366-laptop fit survives via the OTHER half of that
       fix: the 24px side padding below 1440px (see @media below) — at 1366,
       the grid's content is 1366 - 48 - 8 (scrollbar) - 40 (its own sides)
       = 1270px, which holds 3×400 + 2×32 = 1264px.
       The narrow floor, calc(50% - 16px), is two columns that fill the row
       exactly: half the row less half the 32px column gap below. It read
       50% - 14px (half of a 28px gap) and came out 4px unequal under ~1000px
       (338 + 334 at 800), and the second column's paintings 2.7px short,
       because --wall-col is read off the FIRST track (desktop polish round
       3, finding 2c). */
    grid-template-columns: repeat(auto-fill, minmax(min(400px, calc(50% - 16px)), 1fr));
    justify-content: center;  /* Center when cards don't stretch to fill */
    /* Hanging spacing: gallery walls breathe more between rows than between
       neighbouring pieces — vertical gap > horizontal gap. */
    gap: 52px 32px;
    padding: 28px 20px;
    flex: 1;            /* Take remaining space in grid-container */
    align-content: start; /* Align cards to top, not stretch */

    /* ── GALLERY WALL METRICS ──────────────────────────────────────────────
       The card is a hung painting (native 3:2) + a placard beneath it, so its
       height is DETERMINISTIC: one resolved column width (--wall-col,
       published by bindWallMetrics in grid-mode.js — read from the container,
       never measured off cards) derives everything. --wall-pad is the card's
       horizontal inset: it gives the painting's mounted shadow room to paint
       inside the card's own box (content-visibility:auto implies paint
       containment, so nothing may rely on bleeding outside the card).
       THE RUBY SEAT (desktop polish round 3, finding 2a): every word tile's
       headword line holds a furigana line, read or not (the rule beside
       .grid-card-back-japanese). 142px was built on the bare line, the
       headword's ceiling size at line-height 1.25 (1.55rem × 1.25 = 31px);
       the seated line is 3.25rem (52px, the ruby headword's 51.8 rounded
       up). The placard grows by the difference, 21px, so the painting stays
       a full 3:2 in every tile and the ruby is never paid out of the print. */
    --wall-pad: 8px;
    --ruby-seat: calc(3.25rem - 1.55rem * 1.25);
    --placard-h: calc(142px + var(--ruby-seat));
    --painting-h: calc((var(--wall-col, 400px) - var(--wall-pad) * 2) / 1.5);
    --wall-card-h: calc(var(--painting-h) + var(--placard-h));
}

/* Walls that hang no word placard keep the bare one: the hall's journeys
   (#hallWall reuses .grid-cards and its --wall-card-h) and the kanji wall,
   whose cards carry the glyph, not a ruby headword. */
.grid-cards.hall-wall,
body.kanji-path-active .grid-cards {
    --ruby-seat: 0px;
}

/* 3-column hard cap — uniform grid must never exceed masonry's density (3).
   Above ~1684px the 1800px-capped container is wide enough that auto-fill would
   admit a 4th 400px track; pin it to exactly 3 so the density can't regress.
   min-width gate keeps laptops (≤1440px, which top out at 3 anyway) untouched.
   Masonry's JS engine already caps at 3, so this only governs the uniform grid. */
@media (min-width: 1680px) {
    .grid-cards:not(.masonry-layout) {
        grid-template-columns: repeat(3, 1fr);
    }
}

/* =============================================================================
   MASONRY VARIANT (previewable toggle) — content drives card height: short
   monumental cards for 1-2 character words, taller cards for verbs (three
   forms) and long glosses.
   Engine: JS shortest-column placement (layoutMasonry in grid-mode.js),
   NOT CSS multi-columns — balanced columns re-distribute globally on any
   height change, which yanked cards away from their own related drawer.
   Absolute placement is deterministic: opening a drawer shifts only the
   cards directly below in that column, and reading order stays roughly
   row-major (cards fill the currently shortest column, left to right).
   ============================================================= */
.grid-cards.masonry-layout {
    display: block;
    position: relative;
    /* height set inline by layoutMasonry */
}

.grid-cards.masonry-layout .grid-card {
    position: absolute;
    margin: 0;
    aspect-ratio: 1 / 1.02;
    /* width/left/top set inline by layoutMasonry */
    /* content-visibility:auto lets the browser skip layout + paint for the
       off-screen cards. With 1,195 absolutely-placed cards that single CSS
       change drops the grid→masonry reflow from ~430ms to ~20ms (the toggle
       freeze the owner felt). contain-intrinsic-size is set inline per card by
       layoutMasonry (= computed height) so off-screen cards reserve the right
       box and the scroll geometry never jumps. Card heights are deterministic
       from the aspect-ratio tiers below, so layoutMasonry COMPUTES them — it no
       longer reads offsetHeight for every card (the other half of the freeze). */
    content-visibility: auto;
}

/* Grade echo joins the wall as a placed item */
.grid-cards.masonry-layout .grid-card-echo {
    position: absolute;
    margin: 0;
}

/* Push choreography: after the first placement (masonry-settled), cards
   glide to new positions — the drawer opening reads as a column breathing
   open, not a teleport. Initial placement is instant. The card rule keeps
   the base hover transitions (this declaration replaces, not extends). */
.grid-cards.masonry-layout.masonry-settled .grid-card {
    transition:
        transform 0.2s ease-out,
        box-shadow 0.4s cubic-bezier(0.22, 1, 0.36, 1),
        border-color 0.4s cubic-bezier(0.22, 1, 0.36, 1),
        top 0.32s var(--ease-settle, ease-out),
        left 0.32s var(--ease-settle, ease-out);
}

.grid-cards.masonry-layout.masonry-settled .grid-more-card,
.grid-cards.masonry-layout.masonry-settled .grid-end-footer {
    transition: top 0.32s var(--ease-settle, ease-out), left 0.32s var(--ease-settle, ease-out);
}

/* Echo keeps its entrance fade/settle alongside the wall-glide transitions
   (this declaration REPLACES the base .grid-card-echo transition). */
.grid-cards.masonry-layout.masonry-settled .grid-card-echo {
    transition: top 0.32s var(--ease-settle, ease-out), left 0.32s var(--ease-settle, ease-out),
                opacity 0.5s var(--ease-settle, ease), transform 0.5s var(--ease-settle, ease);
}

.grid-cards.masonry-layout .grid-more-card,
.grid-cards.masonry-layout .grid-end-footer {
    position: absolute;
    /* placed by layoutMasonry below the tallest column */
}

/* Variant-only art direction: the content sets the card's breath */
.grid-cards.masonry-layout .grid-card[data-ja-len="1"],
.grid-cards.masonry-layout .grid-card[data-ja-len="2"] {
    aspect-ratio: 1 / 0.76;
}

.grid-cards.masonry-layout .grid-card[data-ja-len="3"] {
    aspect-ratio: 1 / 0.9;
}

/* The en-long / verb height bumps are tuned for the IMAGE face (long gloss,
   three verb forms). In Hide mode (calligraphy face = no .cards-flipped) those
   cards show only a short headword, so a tall card would be mostly empty paper
   — scope the bumps to the image face and let Hide walls breathe by ja-len. */
.grid-cards.masonry-layout.cards-flipped .grid-card[data-en-long] {
    aspect-ratio: 1 / 1.14;
}

.grid-cards.masonry-layout.cards-flipped .grid-card.grid-card-verb {
    aspect-ratio: 1 / 1.18;
}

/* Masonry image fill: the 4:3 watercolors letterbox inside the variant's
   taller/shorter image zones (object-fit: contain leaves dead whitespace).
   In masonry the painting fills its zone edge to edge — container size and
   image size always agree; cover-cropping the scenics reads intentional. */
.grid-cards.masonry-layout .grid-card-back-image img {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    object-fit: cover;
}

/* =============================================================================
   STAGGERED ENTRANCE ANIMATION
   ============================================================================= */

@keyframes cardEntrance {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Card exit animation - two-phase: fade+scale then height collapse
   Phase 1 (0-50%): Visual exit - fade out + scale down
   Phase 2 (50-100%): Height collapse - smooth gap closure
   NOTE: .exiting also sets min-height: 0 !important to override mobile min-height */
@keyframes cardExit {
    0% {
        opacity: 1;
        transform: translateY(0) scale(1);
        height: var(--card-exit-height, 200px);
        margin-bottom: 0;
    }
    40% {
        opacity: 0;
        transform: translateY(-20px) scale(0.85);
        height: var(--card-exit-height, 200px);
        margin-bottom: 0;
    }
    100% {
        opacity: 0;
        transform: translateY(-20px) scale(0.85);
        height: 0;
        /* The flex-column era's gap (28px). The wall is a CSS grid now, whose
           52px row gap and 32px column gap no item margin can eat, so this
           value changes nothing there: the slot closes when the card leaves
           the DOM. (Comment corrected in desktop polish round 3, finding 2c.) */
        margin-bottom: -28px;
        padding: 0;
        border-width: 0;
    }
}

/* Quiet receipt left in the slot after a graded card flies out of the wall
   (the comment said "out of a deck" until 2026-08-16 — the surfaces that can
   fly a card are the browse wall and the 今日 session now, the status decks
   having retired; the receipt itself is unchanged):
   「食べる to eat — また明日 · again tomorrow — Undo」. One line, auto-fades.
   It settles DOWN into the slot (translateY -8px → 0) while the card lifts
   away upward — the receipt reads as left behind by the departing card. */
.grid-card-echo {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 6px 8px;
    padding: 4px 12px;
    min-height: 44px;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 12px;
    color: var(--color-stone, #57534e);
    opacity: 0;
    transform: translateY(-8px);
    transition: opacity 0.5s var(--ease-settle, ease), transform 0.5s var(--ease-settle, ease);
}

.grid-card-echo.echo-visible {
    opacity: 1;
    transform: translateY(0);
}

/* The word inks itself onto the receipt a beat after the line lands. */
.grid-card-echo .grid-echo-word {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 15px;
    color: var(--color-ink, #2C2C2C);
    opacity: 0;
    filter: blur(2px);
    transition: opacity 0.55s ease 0.12s, filter 0.55s ease 0.12s;
}

.grid-card-echo.echo-visible .grid-echo-word {
    opacity: 1;
    filter: blur(0);
}

.grid-card-echo .grid-echo-gloss {
    color: var(--color-stone, #57534e);
}

.grid-card-echo .grid-echo-dash {
    opacity: 0.4;
}

/* On expiry the echo fades and HOLDS its grid cell (a removed node would
   re-pack every later card one slot earlier — a teleport-reshuffle of the
   live wall). The held box must keep its height: never collapse it, or the
   reshuffle returns. Swept by the next renderGrid innerHTML rebuild. */
.grid-card-echo.echo-expired {
    opacity: 0;
    transform: translateY(0);   /* pin — losing .echo-visible would snap back to -8px */
    pointer-events: none;
    transition: opacity 0.4s ease;
}

.grid-card-echo .grid-echo-jp {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 13px;
}

.grid-card-echo .grid-echo-sep {
    opacity: 0.5;
}

.grid-card-echo .grid-echo-undo {
    border: none;
    background: none;
    padding: 2px 6px;
    font: inherit;
    font-size: 12px;
    color: var(--color-accent, #3D5A80);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-color: rgba(61, 90, 128, 0.35);
}

.grid-card-echo .grid-echo-undo:hover {
    text-decoration-color: currentColor;
}

.grid-card.exiting {
    animation: cardExit 0.6s var(--ease-out-expo) forwards !important;
    pointer-events: none;
    overflow: hidden;
    min-height: 0 !important;  /* Override mobile min-height: 160px so height can collapse */
}

/* Paper-lift exit for graded cards that leave an echo: the card is picked up
   (slight rise + grow + a hair of rotation, shadow deepens), then carried up
   and away. No height collapse — the slot stays for the settling receipt. */
@keyframes cardExitLift {
    0% {
        opacity: 1;
        transform: translateY(0) scale(1) rotate(0deg);
        box-shadow: 0 1px 3px rgba(44, 44, 44, 0.05);
    }
    30% {
        opacity: 1;
        transform: translateY(-7px) scale(1.012) rotate(0.5deg);
        box-shadow: 0 10px 24px rgba(44, 44, 44, 0.06);
    }
    100% {
        opacity: 0;
        transform: translateY(-36px) scale(0.97) rotate(1.1deg);
        box-shadow: 0 14px 28px rgba(44, 44, 44, 0.06);
    }
}

.grid-card.exiting-lift {
    animation: cardExitLift 0.55s var(--ease-settle, cubic-bezier(0.22, 1, 0.36, 1)) forwards !important;
    pointer-events: none;
    min-height: 0 !important;
}

.grid-card {
    animation: cardEntrance 0.6s var(--ease-out-expo) backwards;
}

/* Stagger animation — 40ms per card for gentle wave (流れ).
   One rule driven by --card-i (emitted in createCardHTML, capped at 20)
   instead of 20 nth-child rules. Scroll-in cards get a fresh inline
   animation-delay from the entrance observer, which overrides this — fixing
   the old bug where cards 13-20 scrolled into view late still waited 0.8s. */
.grid-card { animation-delay: calc((var(--card-i, 0) + 1) * 0.04s); }

/* =============================================================================
   GRID CARD COMPONENT
   ============================================================================= */

/* =============================================================================
   今日の一枚 — TODAY'S PIECE (the feature hanging)
   One day-seeded word hung at double width at the wall's entrance: painting
   left, museum wall-text right. The wall's only solid gold and the only story
   visible at rest — scarcity is the entire value.
   ============================================================================= */

.grid-feature {
    grid-column: span 2;
    display: flex;
    gap: 28px;
    align-items: stretch;
    cursor: pointer;
    padding: 0 var(--wall-pad, 8px);
    height: var(--wall-card-h, 480px);
    overflow: hidden;          /* mirror the cards' paint box */
    animation: cardEntrance 0.7s var(--ease-out-expo) backwards;
}

/* Hide mode (container loses .cards-flipped) — the wall asks, but the feature
   and the kanji seams ANSWER (word, gloss, story, readings of nearby cards).
   They step off the wall until unhide; class toggle only, no re-render. */
.grid-cards:not(.cards-flipped) .grid-feature,
.grid-cards:not(.cards-flipped) .grid-seam { display: none; }

.gf-art {
    flex: 0 0 58%;
    align-self: center;
    aspect-ratio: 3 / 2;
    position: relative;
    overflow: hidden;
    border-radius: 5px;
    box-shadow:
        0 0 0 1px rgba(44, 44, 44, 0.08),
        0 10px 24px -10px color-mix(in srgb, var(--card-tone, #2c2c2c) 60%, rgba(44, 44, 44, 0.5)),
        0 2px 8px -2px rgba(44, 44, 44, 0.12);
}

.gf-art img {
    width: 100%;
    height: 100%;
    object-fit: cover;          /* 3:2 in 3:2 — a fit, never a crop */
    display: block;
    transition: transform 1.8s cubic-bezier(0.19, 1, 0.22, 1);
}

.grid-feature:hover .gf-art img {
    transform: scale(1.03);
}

/* Gasan — the word inscribed on the painting, vertical white ink with a sumi
   shadow (the word page's premium feeling, miniaturized into the wall). */
.gf-gasan {
    position: absolute;
    top: 22px;
    right: 22px;
    writing-mode: vertical-rl;
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: clamp(28px, 3.2vw, 44px);
    font-weight: 600;
    letter-spacing: 0.12em;
    color: rgba(255, 255, 255, 0.96);
    text-shadow:
        0 1px 3px rgba(25, 22, 18, 0.50),
        0 0 18px rgba(25, 22, 18, 0.28);
    pointer-events: none;
}

.gf-placard {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 12px 8px 12px 0;
}

.gf-eyebrow {
    display: flex;
    align-items: baseline;
    gap: 10px;
    font-family: 'Inter', sans-serif;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--color-gold-deep, #8a6e35);
    margin-bottom: 18px;
}

.gf-eyebrow [lang="ja"] {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 13px;
    letter-spacing: 0.3em;
}

.gf-jp {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: clamp(34px, 3.4vw, 50px);
    font-weight: 600;
    line-height: 1.3;
    color: var(--color-ink);
    font-feature-settings: 'palt' 1;
    margin-bottom: 2px;
}

.gf-jp rt {
    font-size: 0.5em;
    color: rgba(44, 44, 44, 0.65);
}

.gf-romaji {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: 'Inter', sans-serif;
    font-size: 0.95rem;
    font-style: italic;
    letter-spacing: 0.04em;
    color: var(--color-amber-deep);
    margin-bottom: 6px;
}

.gf-audio {
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: 1px solid rgba(44, 44, 44, 0.14);
    border-radius: 50%;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 13px;
    font-style: normal;
    color: var(--color-stone, #57534e);
    cursor: pointer;
    box-shadow: none;
    padding: 0;   /* components.css button padding squeezed the circle into an oval */
    transition: color 0.2s ease, border-color 0.2s ease;
}

/* flex:none — the flex button shrank the icon to 0 width (the old 音 text
   had an unshrinkable text width; a sized svg doesn't) */
.gf-audio svg { width: 14px; height: 14px; display: block; flex: none; }

.gf-audio:hover {
    color: var(--color-ink);
    border-color: rgba(44, 44, 44, 0.32);
    background: none;
    transform: none;
    box-shadow: none;
}

.gf-gloss {
    font-family: 'Inter', sans-serif;
    font-size: 1.15rem;
    font-weight: 500;
    color: rgba(44, 44, 44, 0.82);
    line-height: 1.45;
}

/* The day's only solid gold: one thin rule under the identity block */
.gf-rule {
    width: 44px;
    height: 1px;
    background: var(--color-gold, #C9A96E);
    margin: 20px 0 16px;
}

.gf-story {
    font-family: 'Inter', sans-serif;
    font-size: 0.92rem;
    line-height: 1.65;
    color: rgba(44, 44, 44, 0.66);
    margin: 0 0 18px;
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.gf-open {
    font-family: 'Inter', sans-serif;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgba(44, 44, 44, 0.38);
    transition: color 0.25s ease;
}

.grid-feature:hover .gf-open {
    color: var(--color-gold-deep, #8a6e35);
}

.grid-feature:focus-visible {
    outline: 1.5px solid rgba(61, 90, 128, 0.45);
    outline-offset: 4px;
    border-radius: 6px;
}

/* Compact variant (769–1279px, set by JS at render): laptops meet the day's
   piece too — the same two columns at reduced scale. Type steps down, the
   placard tightens, the story keeps one fewer line; whitespace stays. */
.grid-feature.gf-compact { gap: 20px; }
.grid-feature.gf-compact .gf-art { flex-basis: 50%; }
.grid-feature.gf-compact .gf-eyebrow { margin-bottom: 12px; }
.grid-feature.gf-compact .gf-jp { font-size: clamp(26px, 3.4vw, 38px); }
.grid-feature.gf-compact .gf-gloss { font-size: 1.02rem; }
.grid-feature.gf-compact .gf-rule { margin: 14px 0 12px; }
.grid-feature.gf-compact .gf-story {
    font-size: 0.88rem;
    -webkit-line-clamp: 3;
    margin-bottom: 14px;
}

/* ── The piece in a SHARED row (desktop polish round 3, finding 2b) ─────────
   On a three-column wall the piece spans two tracks and a tile hangs in the
   third, in the same row. There the piece's painting hangs from the row's
   top, the line every tile's painting hangs from, and its text column
   centres on the painting: the piece's box shrinks to its own content
   (align-self: start), so the stretched placard is exactly as tall as the
   painting, and `safe center` starts a text column that runs taller than
   the painting on the same top line instead of letting it climb above it.
   The row keeps the tile's height (the box never grows past it: max-height,
   and the old overflow clip still stands). Alone on a two-column row (1280
   and narrower) the piece keeps its seat, centred in the full card height.
   WHEN THE ROW IS SHARED: three tracks need 3×400 + 2×32 = 1264px of grid,
   after the scroller's sides (24px each to 1440, 40 above), its 8px
   scrollbar and the grid's own 20px sides: from 1360px with no side panel,
   from 1652px with the filter panel or the settings rail (260px) open.
   Keep both numbers in step with the track rule (.grid-cards) and the
   scroller's padding (.grid-container) if either moves. */
@media (min-width: 1360px) {
    body:not(.browse-sidebar-open):not(.settings-sidebar-open) .grid-container .grid-feature {
        height: auto;
        max-height: var(--wall-card-h, 480px);
        align-self: start;
    }
    body:not(.browse-sidebar-open):not(.settings-sidebar-open) .grid-container .grid-feature .gf-art {
        align-self: flex-start;
    }
    body:not(.browse-sidebar-open):not(.settings-sidebar-open) .grid-container .grid-feature .gf-placard {
        justify-content: safe center;
    }
}
@media (min-width: 1652px) {
    .grid-container .grid-feature {
        height: auto;
        max-height: var(--wall-card-h, 480px);
        align-self: start;
    }
    .grid-container .grid-feature .gf-art {
        align-self: flex-start;
    }
    .grid-container .grid-feature .gf-placard {
        justify-content: safe center;
    }
}

/* Below the desktop product the feature simply doesn't render (gated in JS
   too — mobile has its own journey home). */
@media (max-width: 768px) {
    .grid-feature { display: none; }
}

/* =============================================================================
   漢字の縫い目 — KANJI SEAMS
   Museum section text between wall rows: one kanji in ink between whisper-gold
   hairlines, a one-breath hook, and the words on this wall that share it.
   今日の一枚 keeps the wall's only solid gold.
   ============================================================================= */

.grid-seam {
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    padding: 26px 0 10px;
}

.grid-seam-head {
    display: flex;
    align-items: center;
    gap: 22px;
    width: min(60%, 560px);
}

.grid-seam-rule {
    flex: 1;
    height: 1px;
    background: color-mix(in srgb, var(--color-gold, #C9A96E) 38%, transparent);
}

.grid-seam-kanji {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 34px;
    font-weight: 600;
    line-height: 1;
    color: var(--color-ink);
}

.grid-seam-line {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 14px;
    font-family: 'Inter', sans-serif;
    font-size: 12.5px;
    color: var(--color-stone, #57534e);
    max-width: 880px;
    padding: 0 16px;
}

.grid-seam-hook {
    color: rgba(44, 44, 44, 0.55);
    margin-right: 8px;
}

.grid-seam-word {
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: 0;
    box-shadow: none;
    padding: 0;
    cursor: pointer;
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    font-family: 'Inter', sans-serif;
    font-size: 12.5px;
    color: var(--color-stone, #57534e);
    transition: color 0.2s ease;
}

.grid-seam-word:hover,
.grid-seam-word:focus-visible {
    color: var(--color-ink);
    background: none;
    transform: none;
    box-shadow: none;
}

.grid-seam-word [lang="ja"] {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 15px;
    font-weight: 600;
    color: var(--color-ink);
}

.grid-seam-word .gsm-romaji {
    font-style: italic;
    color: var(--color-amber-deep);
}

.grid-seam-word:hover .gsm-en,
.grid-seam-word:focus-visible .gsm-en {
    text-decoration: underline;
    text-decoration-color: rgba(44, 44, 44, 0.3);
    text-underline-offset: 3px;
}

/* the wall's one ring (round 3, finding 10; its halo is already off above) */
.grid-seam-word:focus-visible {
    outline: 2px solid var(--color-accent, #3D5A80);
    outline-offset: 2px;
    border-radius: 3px;
}

.gsm-sep {
    color: var(--color-faint, #a8a29e);
    opacity: 0.7;
}

/* =============================================================================
   GRID CARD COMPONENT - Premium Design
   ============================================================================= */

/* Gallery hanging — the card is no longer a white box: the painting hangs
   directly on the warm stone wall with a placard beneath it. The card element
   itself is an invisible hanging area (the paper surface survives on the
   ASKING face — .grid-card-front — where a card of paper is exactly what a
   self-test should be). */
.grid-card {
    /* Hover wake cascade — the card wakes in three beats instead of seven
       things popping at once: surface (0ms) → knowledge (--wake-2) →
       actions (--wake-3). Delays live ONLY in :hover rules so mouse-leave
       retracts everything instantly. */
    --wake-2: 70ms;
    --wake-3: 140ms;
    position: relative;
    background: transparent;
    border-radius: 0;
    overflow: hidden;
    cursor: pointer;
    height: var(--wall-card-h, 480px);

    transition: margin-top 0.35s cubic-bezier(0.16, 1, 0.3, 1);

    /* Enable 3D transforms — perspective only on flip to avoid 800+ stacking contexts */
    transform-style: preserve-3d;

    /* Stable scroll with 800+ cards — give browser accurate height hints */
    content-visibility: auto;
    contain-intrinsic-size: auto var(--wall-card-h, 480px);
}

/* Keep card stable when clicking the status buttons (prevent hover-lift bounce) */
.grid-card:has(.grid-card-status-btn:active) {
    transform: none !important;
}

/* 窓 windowed wall — the unmounted tail's stand-in. Full-row grid item whose
   height is set inline from the deterministic wall geometry (see
   _wallUpdateSpacer in grid-mode.js); it keeps the scrollbar honest while
   only the head slice of a big set is mounted. Never a .grid-card. */
.grid-wall-spacer {
    grid-column: 1 / -1;
    pointer-events: none;
    /* Never an anchor node: a scrollbar drag into the spacer would otherwise
       make the browser hold the SPACER visually stable while chunks mount
       above it — scrollTop chases the spacer downward and the whole wall
       cascade-mounts (measured: a 22,000px jump landed at 211,059px). With
       anchoring off here, the jump offset holds and the sweep mounts only
       the slice that covers it. */
    overflow-anchor: none;
}

/* Card flip container - Enhanced 3D flip */
.grid-card-inner {
    position: relative;
    width: 100%;
    height: 100%;
    transform-style: preserve-3d;
    /* --ease-page: the marquee gesture gets a designed curve — slow commit,
       fast turn, soft landing — instead of generic ease-out */
    transition: transform var(--grid-flip-duration) var(--ease-page, ease-out);
}

/* Hover-intent layer promote: a flip is the likeliest next act on a hovered
   card — pre-rasterize JUST that card's faces so the 3D turn starts without a
   first-frame jank. One card at a time; blanket will-change on 1,195 cards
   would explode compositor memory. */
.grid-card:hover .grid-card-inner {
    will-change: transform;
}

/* Flipped state — add perspective only when needed to avoid 800+ stacking contexts */
.grid-card.flipped {
    perspective: 1200px;
}
.grid-card.flipped .grid-card-inner {
    transform: rotateY(180deg);
}

/* No-transition helper for bulk operations */
.grid-cards.no-flip-transition .grid-card-inner {
    transition: none !important;
}

/* Bulk 3D flip — all cards show back face */
.grid-cards.cards-flipped .grid-card-inner {
    transform: rotateY(180deg);
}

/* XOR: individually flipped cards cancel out → show front */
.grid-cards.cards-flipped .grid-card.flipped .grid-card-inner {
    transform: rotateY(0deg);
}

.grid-cards.cards-flipped .grid-card-front {
    pointer-events: none;
}

.grid-cards.cards-flipped .grid-card-back {
    pointer-events: auto;
}

/* XOR: cancel pointer events for individually flipped cards */
.grid-cards.cards-flipped .grid-card.flipped .grid-card-front {
    pointer-events: auto;
}

.grid-cards.cards-flipped .grid-card.flipped .grid-card-back {
    pointer-events: none;
}

/* =============================================================================
   問いかけ (TOIKAKE) — DUE-RECALL VEIL
   Due cards boot on the calligraphy face: the first glance is a retrieval
   attempt. A quiet amber thread marks the asking state; it lifts when the
   card is flipped (the retrieval event) or graded.

   The marker follows the ASKING FACE, not the .flipped flip-mechanic — a due
   card shows its asking face two ways: with .flipped while the wall rests
   unhidden, OR without .flipped while the wall is globally hidden (Hide / H /
   Space). Keying off .flipped alone stripped the veil the moment Hide dropped
   .flipped from due cards, leaving a still-due card looking uniformly white.
   So every veil rule lists both states; the hidden-wall variant excludes
   already-lifted cards so a checked card doesn't re-assert the loud marker.
   ============================================================================= */

/* Amber thread along the top edge of the asking face. On the face, not the
   card, so geometry never shifts and the thread lifts with the veil. */
.grid-card.is-due.flipped .grid-card-front,
.grid-cards:not(.cards-flipped) .grid-card.is-due:not(.flipped):not([data-veil-lifted="1"]) .grid-card-front {
    border-top: 2px solid rgba(138, 110, 53, 0.5);
    /* Soft amber breath below the thread — washi catching late light */
    background-image: linear-gradient(to bottom, rgba(138, 110, 53, 0.06), transparent 30%);
}

/* THE QUESTION LINE under the headword — every test surface states its ONE
   question in plain English (reading journey law 15, owner ruling
   2026-07-05; supersedes the hover-only めくる hint — a test must declare
   itself without waiting for a cursor).
   Rendered on every front face but SHOWN only while the veil is asking;
   the wall's ask language picks WHICH question prints (text-flipped = the
   English asks, the learner produces the Japanese). */
.grid-card-veil-hint { display: none; }
.grid-card.is-veiled.flipped .grid-card-front .grid-card-veil-hint,
.grid-cards:not(.cards-flipped) .grid-card.is-veiled:not(.flipped):not([data-veil-lifted="1"]) .grid-card-front .grid-card-veil-hint {
    display: flex;
    align-items: baseline;
    justify-content: center;
    font: 400 12px 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    letter-spacing: 0.02em;
    color: var(--color-stone, #57534e);
    margin-top: 10px;
}
.grid-card-veil-hint .veil-q--produce { display: none; }
.grid-cards.text-flipped .grid-card-veil-hint .veil-q--meaning { display: none; }
.grid-cards.text-flipped .grid-card-veil-hint .veil-q--produce { display: inline; }

/* flipPulse scale(1.05) retired — cards rest, not float; nothing ever added
   the .flipping class, so the pulse was dead code anyway */

/* Disable pointer events on front face when 3D-flipped (desktop) */
.grid-card.flipped .grid-card-front {
    pointer-events: none;
}

.grid-card.flipped .grid-card-back {
    pointer-events: auto;
}

/* Card faces — inset by --wall-pad so the painting's mounted shadow has room
   to paint inside the card's own clip box (content-visibility:auto implies
   paint containment; nothing may rely on bleeding outside the card). */
.grid-card-front,
.grid-card-back {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--wall-pad, 8px);
    right: var(--wall-pad, 8px);
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    display: flex;
    flex-direction: column;
    border-radius: var(--radius-lg);
    overflow: hidden;
}

/* The word face hosts the study seal when it's the face up at grade time (the grade
   buttons live here), so contain the seal's multiply ink-bloom to this face — the
   same isolation the painting (.grid-card-back-image) already has. */
.grid-card-front { isolation: isolate; }

/* The gallery face is not a surface — the painting and placard sit directly
   on the stone. Clipping would amputate the painting's shadow; the closed
   story panel hides via its own opacity/visibility, not this clip. */
.grid-card-back {
    border-radius: 0;
    overflow: visible;
}

/* Status indication handled on back-face children (.grid-card-back-content, .grid-card-rel)
   because card faces are position:absolute and cover the parent — inset shadows on .grid-card are hidden */

.grid-card-back {
    transform: rotateY(180deg);
    background: transparent;
}

/* Status dot — top-right corner, visible at rest, hides on hover as action buttons appear */
.grid-card-back::before {
    content: '';
    position: absolute;
    top: 18px;
    right: 18px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    opacity: 0;
    z-index: 6;
    pointer-events: none;
    transition: opacity 0.25s ease;
}

.grid-card-back.known::before {
    background: var(--color-moss);
    opacity: 0.7;
}

.grid-card-back.learning::before {
    background: var(--color-amber);
    opacity: 0.65;
}

/* Hide dot on hover — action buttons (⛶ ⋯ 🎧) take over the top-right zone */
.grid-card:hover .grid-card-back::before {
    opacity: 0;
}

/* Earned status lives on the art, not on chrome: known/learning tint the
   painting's hairline ring (see .grid-card-back-image below). The old
   left-edge inset bars belonged to the white box and retire with it. */
.grid-card-back.known .grid-card-back-image {
    --ring: rgba(107, 127, 92, 0.38);
}

.grid-card-back.learning .grid-card-back-image {
    --ring: rgba(160, 120, 80, 0.40);
}

/* =============================================================================
   CARD BACK - SPLIT LAYOUT (Image + English)
   ============================================================================= */

/* THE PAINTING — hung first (order:-1 hoists it above the header inside
   back-inner), at its native 3:2 so the full scene always shows: no crop, no
   letterbox, in any state. Every servable image is exactly 1536×1024; if a
   future generation batch changes that, this ratio and --painting-h (in
   .grid-cards) are the two places that know. */
.grid-card-back-image {
    order: -1;
    /* The painting is the placard's shock absorber: when a card's placard
       runs tall (ruby headword + verb 活用 line + relation chip), the
       PAINTING gives up the few px — object-fit crops invisibly — instead
       of flex squeezing the header and overlapping romaji into the gloss
       (the long-standing 9px placard collision on verb cards). */
    flex: 0 1 auto;
    min-height: 0;
    width: 100%;
    aspect-ratio: 3 / 2;
    position: relative;
    overflow: hidden;     /* clips the img to the print's crisp corners */
    isolation: isolate;   /* contain the study seal's multiply ink-bloom to the print */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    min-height: 0;
    border-radius: 5px;
    /* Mounted-print depth: a hairline ring (status-tintable via --ring) and a
       soft drop tinted with the painting's own dominant color, so each piece
       warms the stone beneath it. Negative spread keeps the side bleed inside
       the face's --wall-pad room (paint containment — see .grid-card). */
    box-shadow:
        0 0 0 1px var(--ring, rgba(44, 44, 44, 0.08)),
        0 6px 16px -7px color-mix(in srgb, var(--card-tone, #2c2c2c) 60%, rgba(44, 44, 44, 0.5)),
        0 2px 6px -2px rgba(44, 44, 44, 0.12);
}

/* 判 — the earned hanko: a learned print carries the artist's seal (the
   word's first character in kintsugi gold) on its lower-right corner.
   Scarce by definition — only .known cards show it, and nothing else on the
   wall is gold at rest except 今日の一枚's rule. */
.grid-card-hanko {
    position: absolute;
    right: 10px;
    bottom: 10px;
    width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 14px;
    font-weight: 600;
    line-height: 1;
    color: var(--color-gold-deep, #8a6e35);
    background: rgba(250, 250, 247, 0.78);
    border: 1.5px solid var(--color-gold, #C9A96E);
    border-radius: 5px;
    transform: rotate(-3deg);
    opacity: 0;
    transition: opacity 0.4s ease, transform 0.4s ease;
    pointer-events: none;
}

.grid-card-back.known .grid-card-hanko {
    opacity: 0.95;
    transform: rotate(-3deg) scale(1);
}

/* The print fills its frame edge to edge — at 3:2-in-3:2 cover === contain,
   so this is a fit, never a crop. */
.grid-card-back-image img {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    object-fit: cover;
    border-radius: 0;
}

.grid-card-back-image .grid-card-image-placeholder {
    width: 80px;
    height: 80px;
    background: rgba(61, 90, 128, 0.15);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2rem;
}

/* Color tint overlay - hidden */
.grid-card-back-tint {
    display: none;
}

/* =============================================================================
   CARD FRONT
   ============================================================================= */

.grid-card-image {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    background: rgba(61, 90, 128, 0.03);
    min-height: 0;
    position: relative;
    overflow: hidden;
}

/* Shimmer overlay removed (gradient purge) */
.grid-card-image::before {
    display: none;
}

.grid-card-image img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    border-radius: 12px;
    transition:
        transform var(--grid-transition-medium) var(--ease-out-expo),
        opacity 0.3s ease-out,
        filter 0.3s ease-out;
}

/* Shimmer animation for loading placeholders */
@keyframes shimmer {
    0% { background-position: -200% 0; }
    100% { background-position: 200% 0; }
}

/* Shimmer placeholder on image container while loading */
.grid-card-back-image.img-loading,
.grid-card-image.img-loading {
    background: linear-gradient(
        90deg,
        rgba(61, 90, 128, 0.04) 25%,
        rgba(61, 90, 128, 0.08) 50%,
        rgba(61, 90, 128, 0.04) 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s ease-in-out infinite;
}

/* Dominant-tone wash placeholder — cards with an img_tone paint the slot in
   the image's own palette instead of the generic indigo shimmer, so the
   watercolor develops out of its own wash. (Inline background-color wins the
   color; this kills the gradient + animation.) */
.grid-card-back-image[style*="background-color"] {
    transition: background-color 0.5s ease 0.15s;
}

.grid-card-back-image[style*="background-color"].img-loading {
    background-image: none;
    animation: none;
}

/* Once the image has painted, the wash recedes back to paper */
.grid-card-back-image.img-settled {
    background-color: transparent !important;
}

/* Image loading state - hidden until loaded */
.grid-card-image img.loading,
.grid-card-back-image img.loading {
    opacity: 0;
}

/* Image loaded state - smooth fade in */
.grid-card-image img.loaded,
.grid-card-back-image img.loaded {
    opacity: 1;
    transition: opacity 0.25s ease,
                transform 1.6s cubic-bezier(0.19, 1, 0.22, 1);
}

/* No image zoom on hover — one gesture only (front-face slot, retired) */
.grid-card:hover .grid-card-image img {
    transform: none;
}

/* The painting breathes under the hand — one slow exhale (1.6s out-expo,
   compositor-only). 3.5% of drift inside its own frame, a living print,
   not a zoom; mouse-leave releases it at the same pace. */
.grid-card-back-image img {
    transition: transform 1.6s cubic-bezier(0.19, 1, 0.22, 1);
}

.grid-card:hover .grid-card-back-image img {
    transform: scale(1.035);
}

/* Placeholder when no image */
.grid-card-image-placeholder {
    width: 80px;
    height: 80px;
    background: rgba(61, 90, 128, 0.08);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2rem;
    color: var(--text-secondary, #a0a0a8);
    transition: transform var(--grid-transition-medium) ease;
}

.grid-card:hover .grid-card-image-placeholder {
    transform: scale(1.1) rotate(5deg);
}

/* Front: hide image — clean text-only face, image reveals on flip */
.grid-card-front .grid-card-image {
    display: none;
}

.grid-card-front {
    flex-direction: row;
    align-items: center;
    padding: 12px 16px;
    gap: 12px;
    /* The asking face stays a card of PAPER pinned where the painting hangs —
       flipping a due word is lifting the paper to see the art. The white-box
       chrome that left .grid-card lives on here, where it means something. */
    background: var(--color-card-bright);
    border: 1px solid rgba(44, 44, 44, 0.05);
    border-top-color: rgba(255, 255, 255, 0.7);
    box-shadow:
        0 1px 3px rgba(44, 44, 44, 0.04),
        0 4px 10px -4px rgba(44, 44, 44, 0.06);
}

/* Washi grain under the sumi headword — the one textured surface budget,
   spent on content instead of chrome. Static SVG noise tile (rasterized once,
   cached), never a runtime filter. Desktop only. */
@media (min-width: 769px) {
    .grid-card-front::after {
        content: '';
        position: absolute;
        inset: 0;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
        opacity: 0.05;
        mix-blend-mode: multiply;
        pointer-events: none;
        z-index: 0;
    }
}

.grid-card-front .grid-card-content {
    flex: 1;
    border-top: none;
    padding: 0;
    position: relative;
    min-height: 48px;
}

/* Content area - min-height keeps short words centered, long words can grow */
.grid-card-content {
    min-height: 72px;
    flex-shrink: 0;
    padding: 12px 14px;
    text-align: center;
    border-top: 1px solid rgba(61, 90, 128, 0.08);
    background: rgba(255, 255, 255, 0.15);
    position: relative;
    transition: background 0.3s ease, border-color 0.3s ease;
    /* Center text vertically within content area */
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

/* Content area stays clean — status shown via bottom bar + buttons */

/* Dynamic Japanese text scaling based on character count */
/* Front is text-only so scale generously; back still has image */
.grid-card-japanese {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: clamp(2.2rem, calc(22rem / var(--ja-chars, 5)), 4.5rem);
    font-weight: 600;
    color: var(--grid-japanese-color);
    margin-bottom: 4px;
    line-height: 1.3;
    /* Proportional alternate metrics — true JP kerning instead of full-width
       advance + tracking. (Positive letter-spacing loosened kanji pairs.) */
    font-feature-settings: 'palt' 1;
    /* Ink absorption — sumi bleeding into washi paper */
    text-shadow: 0 0 0.5px rgba(44, 44, 44, 0.12);  /* Lowered from 0.15 */
    transition: color 0.4s ease, text-shadow 0.4s ease;
    /* Allow wrapping for longer words */
    word-break: keep-all;
    overflow-wrap: break-word;
    text-wrap: balance;  /* No orphan second lines on multi-form compounds */
}

/* Monumental tiers for 1-2 character words — the calligraphy face earns a
   hanko moment. (The base clamp pins everything ≤4 chars at 4.5rem.) */
.grid-card[data-ja-len="1"] .grid-card-japanese {
    font-size: 6.5rem;
}

.grid-card[data-ja-len="2"] .grid-card-japanese {
    font-size: 5.2rem;
}

/* Attention sharpening on hover — like focusing your eyes, warm focus */
.grid-card:hover .grid-card-japanese {
    color: #1C1A18;  /* Warm focus state - not neutral */
    text-shadow: 0 0 0.3px rgba(28, 26, 24, 0.15);  /* Deepen on hover */
}

/* Kanji door — each page-gated kanji in the painting (back) headword is its
   own door into the kanji page (mirrors the word page's indigo .wp-kt
   language). The default gallery wall rests on this face, so these are the
   kanji the user hovers. Mouse/hover pointers only: on touch the whole
   painting stays the word's door, so the gallery wall never grows a competing
   per-kanji tap target. Pointing at one kanji lights just that glyph indigo —
   a quiet "this is a door". */
@media (hover: hover) and (pointer: fine) {
    .grid-card-back-japanese .wp-kt {
        cursor: pointer;
        transition: color 0.25s ease;
    }
    .grid-card-back-japanese .wp-kt:hover {
        color: var(--color-accent);
    }
}

/* Front-face audio button — hover reveal on desktop, matches back-face style */
.grid-card-front-audio {
    position: absolute;
    top: 14px;
    right: 16px;
    z-index: 5;
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--color-romaji);
    font-size: 1.1rem;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    opacity: 0;
    transform: scale(0.92);
    /* what moves, and only that: an `all` here animated the keyboard ring
       too (desktop polish round 4, finding 6) */
    transition: opacity 0.25s ease, transform 0.25s ease, background-color 0.2s ease, box-shadow 0.2s ease;
    -webkit-tap-highlight-color: transparent;
    padding: 0;
}

.grid-card-front-audio svg { width: 19px; height: 19px; display: block; }

.grid-card:hover .grid-card-front-audio,
.grid-card:focus-within .grid-card-front-audio {
    opacity: 0.7;
    transform: scale(1);
    transition-delay: var(--wake-3);  /* actions beat */
}

.grid-card-front-audio:hover {
    opacity: 1 !important;
    background: rgba(44, 44, 44, 0.04);
    transition-delay: 0s;  /* direct hover feedback stays instant */
}

.grid-card-front-audio:active {
    transform: scale(0.92);
    transition-duration: 0.1s;
}

.grid-card-front-audio.playing {
    animation: audioPulse 1s ease-out;
}

/* Romanization - hidden by default, revealed on audio click */
.grid-card-front .grid-card-romanization {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 1.4rem;
    font-style: normal;
    font-weight: 500;
    color: var(--grid-romanization-color);
    margin-top: 4px;
    padding-right: 4px;  /* Prevent italic overhang clipping */
    opacity: 0;
    max-height: 0;
    overflow: hidden;
    transform: translateY(-5px);
    transition:
        opacity var(--grid-transition-medium) ease,
        max-height var(--grid-transition-medium) ease,
        transform var(--grid-transition-medium) var(--ease-out-expo);
}

/* Show romanization when audio has been played - smooth reveal */
.grid-card.audio-played .grid-card-front .grid-card-romanization {
    opacity: 1;
    max-height: 2em;
    transform: translateY(0);
}

.grid-card-english {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 1.35rem;  /* Down from 1.5rem — less decorative */
    font-weight: 400;  /* Down from 500 — secondary to image */
    color: var(--color-stone);  /* Warm stone — not coral */
    letter-spacing: 0.01em;
    line-height: 1.4;
}


/* Focus button — top-right, left of story + audio buttons, appears on hover */
/* ⛶ — sibling of the 音 chip: same museum-glass washi disc, same size, same
   reveal beat, so the painting's top-right corner reads as ONE quiet pair of
   controls (owner round 1: the bare glyph was invisible on busy scenes). */
.grid-card-focus-btn {
    position: absolute;
    top: 12px;
    right: 100px;
    width: 34px;
    height: 34px;
    border: none;
    border-radius: 50%;
    background: rgba(250, 250, 247, 0.88);
    box-shadow: 0 1px 4px rgba(25, 22, 18, 0.18);
    color: var(--color-ink);
    font-size: 1rem;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    z-index: 5;
    opacity: 0;
    transform: scale(0.92);
    transition: opacity 0.25s ease, transform 0.25s ease, background-color 0.2s ease, box-shadow 0.2s ease;
    -webkit-tap-highlight-color: transparent;
    padding: 0;
}

.grid-card:hover .grid-card-focus-btn,
.grid-card:focus-within .grid-card-focus-btn {
    opacity: 0.92;
    transform: scale(1);
    transition-delay: var(--wake-3);  /* actions beat */
}

.grid-card-focus-btn:hover {
    opacity: 1 !important;
    background: rgba(250, 250, 247, 0.96);
    transition-delay: 0s;  /* direct hover feedback stays instant */
}

.grid-card-focus-btn:active {
    transform: scale(0.92);
    transition-duration: 0.1s;
}

/* Front-face focus button — left of audio button, hover reveal */
.grid-card-front-focus {
    right: 16px;
}

.grid-card-front:has(.grid-card-front-audio) .grid-card-front-focus {
    right: 60px;
}

/* =============================================================================
   TEXT FLIP — CSS overlay swap (Phase 1)
   Only targets front-face elements. Back face is NEVER affected.
   ============================================================================= */

/* English text overlay — hidden by default, positioned over Japanese */
.grid-card-english-swap {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    flex-direction: column;  /* English over its hover-only identity line */
    align-items: center;
    justify-content: center;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 1.3rem;
    font-weight: 500;
    color: var(--color-ink);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
    padding: 0 8px;
    text-align: center;
    word-break: normal;
    overflow-wrap: break-word;
}

/* Text-flipped state — swap Japanese for English on front face only */
.grid-cards.text-flipped .grid-card-japanese {
    opacity: 0;
    transition: opacity 0.15s ease;
}

.grid-cards.text-flipped .grid-card-english-swap {
    opacity: 1;
}

.grid-cards.text-flipped .grid-card-front .grid-card-romanization {
    display: none;
}

/* Recall-prompt identity line — POS + politeness under the English gloss.
   Only meaningful when the English is the prompt, so it lives INSIDE the
   text-flip swap container: it inherits the container's opacity, so it stays
   invisible unless .text-flipped is on. Hover-only on top of that, an opacity
   reveal on the same cascade beat as the back-face meta strip — the resting
   prompt stays a single clean word. Its space is reserved at rest (the column
   reserves room), so the fade-in never nudges the English. */
.grid-card-front-meta {
    margin-top: 9px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.04em;
    line-height: 1;
    text-transform: uppercase;
    color: var(--color-stone);
    opacity: 0;
    transition: opacity 280ms cubic-bezier(0.22, 1, 0.36, 1);
}

.grid-card:hover .grid-card-front-meta,
.grid-card:focus-within .grid-card-front-meta {
    opacity: 1;
    pointer-events: auto;  /* let the register tooltip surface on hover */
    transition-delay: var(--wake-2, 0ms);  /* knowledge beat of the hover cascade */
}

/* POS = dictionary annotation voice — Inter lowercase, quiet warm grey, the
   same treatment it gets in the back-face meta strip. */
.grid-card-front-meta .meta-pos {
    text-transform: lowercase;
    font-weight: 400;
    letter-spacing: 0;
    color: var(--color-faint-warm);
}

.grid-card-front-meta .meta-sep {
    color: var(--color-faint);
    opacity: 0.5;
}

/* =============================================================================
   FRONT-FACE STATUS BUTTONS (Phase 2)
   ============================================================================= */

.grid-card-front-actions {
    display: flex;
    gap: 6px;
    align-items: center;
    z-index: 2;
    position: absolute;
    bottom: 10px;
    right: 12px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
}

.grid-card:hover .grid-card-front-actions {
    opacity: 1;
    pointer-events: auto;
}

/* Status buttons — override global button styles (components.css: shadow, hover lift) */
.grid-card-status-btn,
.grid-card-status-btn:hover,
.grid-card-status-btn:active {
    box-shadow: none;
    transform: none;
}

/* Card chrome icon buttons are flat 落款-quiet circles — opt out of the
   global raised-button shadow (components.css:385) so they rest, not float */
.grid-card-front-audio,
.grid-card-front-audio:hover,
.grid-card-front-audio:active,
.grid-card-back-audio,
.grid-card-back-audio:hover,
.grid-card-back-audio:active,
.grid-card-focus-btn,
.grid-card-focus-btn:hover,
.grid-card-focus-btn:active {
    box-shadow: none;
}

/* Status buttons — the word page's framed verb language (SAVE 残す / SKIP
   流す), scaled for the wall: a quiet dot at rest, the frame wakes on card
   hover. One color grammar app-wide: amber = still learning (save),
   moss = known (skip) — matching the front-face status dots. */
.grid-card-status-btn {
    height: 28px;
    border-radius: 3px;
    border: 1px solid transparent;
    background: transparent;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 0;
    font-family: 'Inter', system-ui, sans-serif;
    opacity: 0.5;
    transition: opacity 0.25s ease, border-color 0.25s ease, background 0.25s ease, color 0.25s ease, padding 0.25s ease, gap 0.25s ease;
    -webkit-tap-highlight-color: transparent;
    flex-shrink: 0;
    padding: 0 5px;
    overflow: hidden;
    white-space: nowrap;
}
.grid-card-status-btn.learning { --vac: var(--color-amber); }
.grid-card-status-btn.known    { --vac: var(--color-moss); }

/* Rest state: one small stone in the verb's color */
.grid-card-status-btn .status-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--vac);
    transition: width 0.25s ease, opacity 0.25s ease;
}

/* The framed form's two voices — hidden until the card wakes */
.grid-card-status-btn .cap {
    max-width: 0;
    opacity: 0;
    overflow: hidden;
    font-size: 9px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: #44403a;
    transition: max-width 0.25s ease, opacity 0.2s ease;
}
.grid-card-status-btn .jp {
    max-width: 0;
    opacity: 0;
    overflow: hidden;
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 11.5px;
    line-height: 1;
    color: var(--vac);
    transition: max-width 0.25s ease, opacity 0.2s ease;
}

/* Card hover/focus: dot gives way to the frame */
.grid-card:hover .grid-card-status-btn,
.grid-card:focus-within .grid-card-status-btn {
    border-color: color-mix(in srgb, var(--vac) 38%, transparent);
    opacity: 0.85;
    padding: 0 11px;
    gap: 7px;
    transition-delay: var(--wake-3);  /* actions beat */
}
.grid-card:hover .grid-card-status-btn .status-dot,
.grid-card:focus-within .grid-card-status-btn .status-dot {
    width: 0;
    opacity: 0;
    transition-delay: var(--wake-3);
}
.grid-card:hover .grid-card-status-btn .cap,
.grid-card:focus-within .grid-card-status-btn .cap,
.grid-card:hover .grid-card-status-btn .jp,
.grid-card:focus-within .grid-card-status-btn .jp {
    max-width: 50px;
    opacity: 1;
    transition-delay: var(--wake-3);  /* actions beat */
}

/* Individual button hover */
.grid-card-status-btn:hover {
    border-color: var(--vac);
    background: color-mix(in srgb, var(--vac) 10%, transparent);
    opacity: 1;
    transition-delay: 0s;  /* direct hover feedback stays instant */
}

/* ONE VISIBILITY LAW (owner 2026-07-22, superseding the browse half of
   2026-07-02 and the deck always-on of 2026-07-14): the grade pair behaves
   identically wherever a card can be marked — it RIDES THE HOVER REVEAL.
   The browse wall shows the verbs only while the card is hovered (or holds
   focus); the mouse leaves, the verbs leave. (The four status decks were the
   law's other hover surface until they retired 2026-08-16 — the law did not
   change, it just has one fewer wall to hold.) The 今日 session is
   the one exception: there grading is the card's whole point (the walk cannot
   advance without it), so its verbs stay framed — see .session-live below.
   Touch screens have no hover; the @media (hover: none) block at the end of
   this section keeps the pair always visible there (iPad is a primary
   device — a hover-only control would be unreachable).
   RECALL GATE: grade only on the REVEALED answer face, never the question face —
   so the front-face grade buttons are hidden everywhere (a study card must be
   flipped to its answer before it can be graded, matching the kanji study card). */
.grid-card-front-actions { display: none; }
/* …and the gate holds for the keyboard too: while a veiled card is still
   asking, its answer-face buttons leave the tab order — backface-visibility
   hides them from the eye but NOT from Tab/Enter, so without this a keyboard
   user could grade sight-unseen. Lifting the veil restores them. */
.grid-card.is-veiled:not([data-veil-lifted="1"]) .grid-card-back-actions,
.grid-card.is-veiled:not([data-veil-lifted="1"]) .grid-card-room-seal {
    visibility: hidden;
}
/* …and the same gate for the HIDE state (one-law review 2026-07-22): with
   browse's back-actions no longer display:none'd, a Hide-mode wall (question
   faces up — cards-flipped XOR flipped says which face shows) would leave
   the rotated-away grade buttons focusable: backface-visibility hides them
   from the eye but NOT from Tab, so Enter could grade sight-unseen.
   visibility:hidden removes them from the tab order whenever the ANSWER
   face is not the one showing. */
.grid-cards:not(.cards-flipped) .grid-card:not(.flipped) .grid-card-back-actions,
.grid-cards.cards-flipped .grid-card.flipped .grid-card-back-actions {
    visibility: hidden;
}
/* …and the same gate for every other control a turned face carries (desktop
   polish round 3). The asking face's 音 (.grid-card-front-audio) stood in the
   Tab order on every tile while the painting face was up: one stop per tile
   on a control nobody could see. The mirror case is the painting face's 音
   and 陽 discs while a Hide-mode wall shows the asking face. Each control
   leaves the tab order whenever its own face is turned away. Word tiles
   only: a kanji card has no faces and keeps its 陽 door. */
.grid-cards.cards-flipped .grid-card:not(.flipped) .grid-card-front-audio,
.grid-cards:not(.cards-flipped) .grid-card.flipped .grid-card-front-audio,
.grid-cards:not(.cards-flipped) .grid-card:not(.grid-card--kanji):not(.flipped) .grid-card-back-audio,
.grid-cards:not(.cards-flipped) .grid-card:not(.grid-card--kanji):not(.flipped) .grid-card-room-seal,
.grid-cards.cards-flipped .grid-card:not(.grid-card--kanji).flipped .grid-card-back-audio,
.grid-cards.cards-flipped .grid-card:not(.grid-card--kanji).flipped .grid-card-room-seal {
    visibility: hidden;
}

/* 今日 SESSION: grading is the card's whole point (the walk cannot advance
   without an answer), so its verbs alone stay VISIBLE at rest and never hide
   behind the hover reveal.
   ONE DRESS (owner amendment 2026-07-22, the lettuce-card ruling): the
   framed full-width pair below is the pair's ONLY dress, worn identically
   in browse and 今日 — visibility is the only thing the two surfaces vary.
   The old browse dot-wake (5px stone → hairline frame on button hover) is
   retired with the compact form; its rules above are inert under these
   later, more specific ones. In the 今日 session the browse chrome cells
   (category · related · 活用) also step off — the kanji card has no such
   chrome, so neither does a word card in study. Browse keeps them.
   (The three rules here read `:not(.deck-live)` until 2026-08-16: the
   status decks were the third surface, a browse FILTER wearing study verbs,
   and they carved themselves out. The decks are retired, .deck-live can no
   longer be set on anything, and the exception went with them. Only
   .session-live is left, so 今日 is the only wall these rules dress.) */
.grid-cards.session-live .grid-card .grid-card-meta-row {
    display: flex;
    opacity: 1;
    pointer-events: auto;
    padding: 6px 4px 12px;
}
/* One dress needs one layout: the action row is a FLEX row wall-wide (the
   browse base in freq-tier.css is a 3-column grid, which parks the pair
   content-sized in the right column — the compact look the owner rejected).
   Flex + empty-cell removal below gives the pair the deck's full-width seat
   everywhere; category / 活用 chrome, when present, shares the row exactly
   as deck cards always did. */
.grid-cards .grid-card .grid-card-meta-row {
    display: flex;
    padding: 6px 4px 12px;
}
.grid-cards.session-live .grid-card-meta-row .meta-cell-left,
.grid-cards.session-live .grid-card-meta-row .meta-cell-center { display: none; }
/* Study answer face is LEAN (owner 2026-07-02): the browse-only gallery chrome
   steps off so a word study card matches the kanji study card's spare answer —
   art + word + gloss + status. The POS/register/freq chip strip is reference
   and stays out of the 今日 session. (It used to stay IN the status decks —
   they were browse wearing study verbs; the decks retired 2026-08-16 and the
   carve-out went with them.) The 陽だまり room seal is NOT chrome (owner amendment
   2026-07-06): it is the door to the word's family at exactly the moment the
   answer is open — hooking a just-revealed word to the ones it lives beside is
   teaching, so the seal now stays in study. Leak-safe by construction: the
   seal lives only in the answer-face markup, and the veiled-ask keyboard gate
   above holds it out of the tab order until the reveal. */
.grid-cards.session-live .grid-card-meta-top { display: none; }
.grid-cards .grid-card .grid-card-back-actions {
    opacity: 1;
    flex: 1 1 auto;
    gap: 8px;
}

/* Empty triptych cells step out of the flex row so the grade pair keeps its
   clean full-width seat on non-verb cards (wall-wide since the one dress).
   (The trailing line — "the 活用 handle keeps its own second-beat hover gate
   in decks" — described .deck-live's centre-cell wake, retired below with
   the status decks on 2026-08-16. There is no second-beat gate left.) */
.grid-cards .grid-card-meta-row .meta-cell:empty { display: none; }
/* (the .deck-live centre-cell wake — the status decks' one deviation from
   browse: keep the 活用/related cell, but let it ride the hover cascade
   instead of resting open — retired 2026-08-16 with the decks. Nothing can
   carry .deck-live now: its only writer was a toggle on window._activeQueue,
   whose only writer was the deck buttons.) */
.grid-cards .grid-card .grid-card-status-btn,
.grid-cards .grid-card .grid-card-status-btn.active {
    flex: 1 1 0;
    justify-content: center;
    height: auto;
    padding: 8px 6px;
    border-radius: 7px;
    border: 1px solid rgba(44, 44, 44, 0.14);
    background: transparent;
    gap: 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary, #57534e);
    opacity: 1;
}
.grid-cards .grid-card .grid-card-status-btn .status-dot,
.grid-cards .grid-card .grid-card-status-btn.active .status-dot {
    width: 7px;
    height: 7px;
    background: currentColor;
    opacity: 0.45;
}
.grid-cards .grid-card .grid-card-status-btn .cap,
.grid-cards .grid-card .grid-card-status-btn .jp {
    max-width: none;
    opacity: 1;
    color: inherit;
}
.grid-cards .grid-card .grid-card-status-btn .cap { font-size: 11px; }
.grid-cards .grid-card .grid-card-status-btn .jp {
    font-size: 13px;
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0.02em;
}
.grid-cards .grid-card .grid-card-status-btn.learning:hover,
.grid-cards .grid-card .grid-card-status-btn.learning:focus-visible {
    border-color: var(--color-amber, #A07850);
    color: var(--color-amber-deep, #8A6E35);
    background: color-mix(in srgb, var(--color-amber, #A07850) 10%, transparent);
}
.grid-cards .grid-card .grid-card-status-btn.known:hover,
.grid-cards .grid-card .grid-card-status-btn.known:focus-visible {
    border-color: var(--color-moss, #6B7F5C);
    color: var(--color-moss, #6B7F5C);
    background: color-mix(in srgb, var(--color-moss, #6B7F5C) 8%, transparent);
}
/* RECEIPT LAW (owner 2026-07-22, superseding the 2026-07-14 "the filled verb
   IS the word's state" deck ruling): .active is a PRESS RECEIPT, never a
   state display — the statusline above the pair is the only place a word's
   standing is spoken. JS lights a verb only on the press itself (grade
   handlers, updateCardStatus fromGrade); every render and reconcile rests
   the pair neutral, so a card never greets the learner with an answer that
   looks already given. These frames dress the receipt. */
.grid-cards .grid-card .grid-card-status-btn.learning.active {
    border-color: color-mix(in srgb, var(--color-amber, #A07850) 55%, transparent);
    background: color-mix(in srgb, var(--color-amber, #A07850) 10%, transparent);
    color: var(--color-amber-deep, #8A6E35);
}
.grid-cards .grid-card .grid-card-status-btn.known.active {
    border-color: color-mix(in srgb, var(--color-moss, #6B7F5C) 55%, transparent);
    background: color-mix(in srgb, var(--color-moss, #6B7F5C) 8%, transparent);
    color: var(--color-moss, #6B7F5C);
}

/* 息 — a same-state claim answers with one quiet breath on the lit button:
   nothing changed, nothing was written, the press "presses on". A ring of
   the button's own ink, in and out once. (claims law, owner 2026-07-14) */
.grid-card-status-btn.verb-affirm {
    animation: verb-affirm-breath 0.55s ease-out;
}
@keyframes verb-affirm-breath {
    0%   { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
    40%  { box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 22%, transparent); }
    100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
    .grid-card-status-btn.verb-affirm { animation: none; }
}

/* .active = the press receipt (receipt law above) — worn from the press
   until the card re-renders, flies, or a reconcile rests it */
.grid-card-status-btn.active {
    border-color: color-mix(in srgb, var(--vac) 70%, transparent);
    background: color-mix(in srgb, var(--vac) 12%, transparent);
    opacity: 0.9;
    padding: 0 11px;
    gap: 7px;
}
.grid-card-status-btn.active .status-dot { width: 0; opacity: 0; }
.grid-card-status-btn.active .cap,
.grid-card-status-btn.active .jp {
    max-width: 50px;
    opacity: 1;
}

/* ── State-aware mark buttons + placard status line (owner direction B, 2026-06-25) ──
   The loved gesture is untouched (quiet colored stone at rest → hairline frame +
   kanji-flavored label on wake). The ONLY change the longer labels force is a
   wider English reveal; the single-kanji anchor (学 study / 覚 remember) keeps its
   50px. Sentence case keeps a 2-3 word phrase calm rather than shouted. */
.grid-card-status-btn .cap {
    letter-spacing: 0.1em;   /* a hair tighter than the 0.16em default so the longer phrase fits; stays UPPERCASE like the original */
}
.grid-card:hover .grid-card-status-btn .cap,
.grid-card:focus-within .grid-card-status-btn .cap,
.grid-card-status-btn.active .cap {
    max-width: 132px;
}

/* The placard's always-visible status line: a stone + word (New carries none —
   an untouched word has no journey yet), in the card's own eyebrow type. Amber
   = Saved, moss = Resting, brighter moss = Coming back, matching the painting's
   dot + ring + the buttons. Same three words the mobile study pill uses.
   Sits quietly under the gloss; the action buttons still wake on hover below. */
.grid-card-statusline {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 6px;
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 9px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: #6b6660;
    white-space: nowrap;
    /* Owner 2026-06-26: the placard status line (STUDYING / RESTING) is hidden at
       rest and wakes only on hover/focus, like the card's other elements. */
    opacity: 0;
    transition: opacity 0.25s ease;
}
.grid-card:hover .grid-card-statusline,
.grid-card:focus-within .grid-card-statusline {
    opacity: 1;
}
.grid-card-statusline .state-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
}
/* New carries NO marker (owner 2026-06-27, Option A): "New" is the empty state —
   it lands on most cards, so a badge there is decoration, not a signal. Silence =
   new. The line appears only once a word has a real journey (Saved / Resting /
   Coming back), so the marker earns its place (scarcity gives meaning).
   display:none, not opacity, so it never takes hover space or width; the
   live-update path (grid-mode.js updateCardStatus) swaps state-* classes, so
   reverting to new re-hides. */
.grid-card-statusline.state-new { display: none; }
.grid-card-statusline.state-saved .state-dot { background: var(--color-amber, #A07850); }
.grid-card-statusline.state-known .state-dot { background: var(--color-moss, #6B7F5C); }
.grid-card-statusline.state-saved { color: color-mix(in srgb, var(--color-amber, #A07850) 62%, #6b6660); }
.grid-card-statusline.state-known { color: color-mix(in srgb, var(--color-moss, #6B7F5C) 62%, #6b6660); }
/* Coming back = a mastered word now due for review — the resting green, woken up.
   A brighter moss so it reads "act now" next to the calm Resting. */
.grid-card-statusline.state-comeback .state-dot { background: var(--color-moss-bright, #5B8C5A); }
.grid-card-statusline.state-comeback { color: color-mix(in srgb, var(--color-moss-bright, #5B8C5A) 70%, #6b6660); }
/* Goodbye line on a flying study card ("Got it · back in 14 days") — GREEN
   like the kanji study card's ladder Got-it (owner 2026-07-03): the colour
   answers the action, the schedule text says when the word returns. */
.grid-card-statusline.state-gotit .state-dot { background: var(--color-moss, #6B7F5C); }
.grid-card-statusline.state-gotit { color: color-mix(in srgb, var(--color-moss, #6B7F5C) 62%, #6b6660); }

/* 重み (Weighted) — the study seal pressed with real MASS (owner-chosen via the
   premium design fleet, 2026-06-25). On a session grade the seal MATERIALIZES poised
   above the print's lower-right, DESCENDS with accelerating weight, lands with a firm
   thunk that over-compresses into the paper, and releases a DEEP wet ink-bloom that
   soaks into the watercolor (multiply, fractalNoise-frayed) PLUS a soft cast shadow
   that grows on contact — so the seal reads as a solid object resting ON the paint.
   The painting sinks a hair under the load, then it HOLDS STILL and flies away with
   the card. Material depth via a raised, lit, beveled ring; each press rests a hair
   crooked (--stamp-crook, seeded). Two inks: moss 済 = "I know it", amber 新 = "save
   to study". Premium = unhurried weight, not flat & quick. Nothing persists. */
.grid-card-stamp {
    --seal-ink: 107, 127, 92;            /* moss — "I know it" (mark-known) */
    --seal-deep: 78, 96, 66;             /* carved-depth partner tone (the glyph sits deep) */
    position: absolute;                  /* left/top set inline (lower-right of the print) */
    width: 60px;
    height: 60px;
    margin: -30px 0 0 -30px;             /* centre the seal on its placed point */
    z-index: 22;
    pointer-events: none;
    transform-origin: 50% 60%;           /* mass pivots low, toward the contact point */
    will-change: transform, opacity, filter;
    transform: rotate(var(--stamp-crook, 2deg));
}
.grid-card-stamp.mark-review {
    --seal-ink: 160, 120, 80;            /* amber — "save to study" */
    --seal-deep: 120, 86, 52;
}

/* soft cast shadow — a SIBLING that stays on the paper as the seal descends onto it,
   so the seal reads as a solid object (not a flat decal). Grows hard at contact. */
.grid-card-stamp-shadow {
    position: absolute;
    width: 60px;
    height: 60px;
    margin: -30px 0 0 -30px;
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(34, 30, 24, 0.30), transparent 72%);
    mix-blend-mode: multiply;
    opacity: 0;
    transform: scale(0.6);
    z-index: 21;
    pointer-events: none;
    will-change: opacity, transform;
}

/* the two-layer wet flood — a dark CORE carries the saturation, a frayed HALO
   gives a living wet edge. Both multiply into the watercolor; alphas (--core-a /
   --halo-a) are capped per-press in JS so the ink never muddies the painting. The
   two fray on DIFFERENT fractalNoise seeds, so they never bleed in lockstep (which
   would read as a printed ring, not a soak). */
.grid-card-stamp .gcs-bloom {
    position: absolute;
    left: 50%;
    top: 50%;
    pointer-events: none;
    mix-blend-mode: multiply;
    -webkit-mask-size: cover;
    mask-size: cover;
    transform: scale(0);
    opacity: 0;
    z-index: 1;
}
.grid-card-stamp .gcs-core {
    width: 92px;
    height: 92px;
    margin: -46px 0 0 -46px;
    background: radial-gradient(closest-side,
        rgba(var(--seal-deep), var(--core-a, 0.27)) 0%,
        rgba(var(--seal-ink),  calc(var(--core-a, 0.27) * 0.66)) 38%,
        rgba(var(--seal-ink),  calc(var(--core-a, 0.27) * 0.26)) 66%,
        transparent 80%);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.74' numOctaves='4' seed='11' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.72 0.72 0.72 0 -0.30'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.74' numOctaves='4' seed='11' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.72 0.72 0.72 0 -0.30'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}
.grid-card-stamp .gcs-halo {
    width: 168px;
    height: 168px;
    margin: -84px 0 0 -84px;
    background: radial-gradient(closest-side,
        rgba(var(--seal-ink), var(--halo-a, 0.11)) 0%,
        rgba(var(--seal-ink), calc(var(--halo-a, 0.11) * 0.5)) 52%,
        transparent 76%);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.54' numOctaves='5' seed='29' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.62 0.62 0.62 0 -0.20'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.54' numOctaves='5' seed='29' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.62 0.62 0.62 0 -0.20'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* the carved chop (落款), outline treatment (縁取り) — the painting shows through
   most: NO solid disc, just a faint paper-light halo to seat the glyph, a STRONG
   carved ring with a light separating halo so the seal's edge reads on any painting,
   and the glyph in the seal ink with a tight light knockout halo for legibility.
   (Owner 2026-06-26: the prior translucent chop dissolved into the art.) */
.grid-card-stamp .gcs-chop {
    position: relative;
    width: 60px;
    height: 60px;
    z-index: 2;
}
.grid-card-stamp .gcs-plate {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    /* only a faint paper-light halo to seat the glyph — NOT a solid disc, so the
       painting stays visible under the mark */
    background: radial-gradient(circle at 50% 50%, rgba(250, 247, 240, 0.55) 0%, rgba(250, 247, 240, 0.30) 44%, transparent 66%);
}
.grid-card-stamp .gcs-keiline {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 3px solid rgb(var(--seal-ink));
    /* a light halo just outside + inside the ring lifts it off the painting */
    box-shadow:
        0 0 0 1.5px rgba(250, 247, 240, 0.55),
        inset 0 0 0 1px rgba(250, 247, 240, 0.45),
        inset 0 1px 0 rgba(255, 255, 255, 0.22),
        inset 0 -1px 0 rgba(var(--seal-deep), 0.22);
}
.grid-card-stamp .gcs-rule {
    display: none;                          /* outline keeps the seal face clean */
}
.grid-card-stamp .gcs-tick {
    position: absolute;
    top: 8px;
    right: 9px;
    width: 5px;
    height: 1.5px;
    border-radius: 1px;
    background: rgba(var(--seal-deep), 0.7);
    transform: rotate(38deg);
}
.grid-card-stamp .gcs-glyph {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'Klee One', 'Hiragino Mincho ProN', 'Noto Sans JP', serif;
    font-size: 29px;
    font-weight: 600;
    line-height: 1;
    color: rgb(var(--seal-ink));         /* the seal ink, not the deep tone — reads brighter on the painting */
    letter-spacing: -0.02em;
    transform: translateY(-0.5px) scaleX(0.94) scaleY(1.02);   /* condensed seal-script set */
    /* a tight light knockout halo around the strokes so 済 / 新 read on busy or dark art */
    text-shadow:
        0 0 2.5px rgba(250, 247, 240, 0.95),
        0 0 1.5px rgba(250, 247, 240, 0.95),
        1px 0 1px rgba(250, 247, 240, 0.8),
        -1px 0 1px rgba(250, 247, 240, 0.8),
        0 1px 0 rgba(var(--seal-deep), 0.30);
    user-select: none;
}
/* the drying sheen — one soft specular band sweeps the wet glyph at contact then
   dries to matte (soft-light keeps it a sheen ON the ink, never a white smear). */
.grid-card-stamp .gcs-sheen {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    overflow: hidden;
    pointer-events: none;
    mix-blend-mode: soft-light;
    opacity: 0;
    z-index: 3;
}
.grid-card-stamp .gcs-sheen::before {
    content: '';
    position: absolute;
    top: -45%;
    left: -130%;
    width: 96%;
    height: 190%;
    transform: rotate(22deg);
    background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.50) 42%, rgba(255, 255, 255, 0.82) 50%, rgba(255, 255, 255, 0.50) 58%, rgba(255, 255, 255, 0) 100%);
    filter: blur(1.8px);
}

/* ===== the weighted gesture: poised → accelerating drop → firm thunk → heavy settle ===== */
.grid-card-stamp.pressing {
    animation: gcStampDrop 560ms cubic-bezier(0.62, 0.02, 0.30, 1) both;
}
@keyframes gcStampDrop {
    0%   { transform: rotate(var(--stamp-crook, 2deg)) translateY(-26px) scale(1.20); filter: blur(2.2px); opacity: 0; }
    14%  { transform: rotate(var(--stamp-crook, 2deg)) translateY(-23px) scale(1.17); filter: blur(1.6px); opacity: 1; }
    26%  { transform: rotate(var(--stamp-crook, 2deg)) translateY(-18px) scale(1.13); filter: blur(1.1px); opacity: 1; }
    52%  { transform: rotate(var(--stamp-crook, 2deg)) translateY(-6px)  scale(1.06); filter: blur(0.9px); }
    66%  { transform: rotate(var(--stamp-crook, 2deg)) translateY(2px)   scale(0.93); filter: blur(0); }
    78%  { transform: rotate(var(--stamp-crook, 2deg)) translateY(-1px)  scale(1.015); filter: blur(0); }
    88%  { transform: rotate(var(--stamp-crook, 2deg)) translateY(0.4px) scale(0.997); filter: blur(0); }
    100% { transform: rotate(var(--stamp-crook, 2deg)) translateY(0)     scale(1);     filter: blur(0); }
}
/* the flood releases AT contact: the CORE floods deep, holds, then sets; the HALO
   chases it a beat later with a sheerer, frayed bleed. Both soak into the paper. */
.grid-card-stamp .gcs-core.soaking {
    animation: gcStampCore 720ms cubic-bezier(0.16, 0.72, 0.30, 1) both;
}
@keyframes gcStampCore {
    0%   { transform: scale(0.14); opacity: 0; }
    18%  { transform: scale(0.42); opacity: 0; }   /* nothing until the seal lands */
    40%  { transform: scale(0.78); opacity: 1; }   /* deep wet peak */
    66%  { transform: scale(0.98); opacity: 0.9; }
    100% { transform: scale(1.04); opacity: 0; }   /* soaks into the paper */
}
.grid-card-stamp .gcs-halo.soaking {
    animation: gcStampHalo 760ms cubic-bezier(0.16, 0.70, 0.30, 1) both;
    animation-delay: 70ms;
}
@keyframes gcStampHalo {
    0%   { transform: scale(0.22); opacity: 0; }
    24%  { transform: scale(0.50); opacity: 0; }
    46%  { transform: scale(0.80); opacity: 1; }
    100% { transform: scale(1.08); opacity: 0; }   /* spreads + dries */
}
/* one sheen pass: the carved glyph glistens as it lands, dried to matte by ~640ms
   (delayed so the gloss appears WITH the thunk, not during the descent) */
.grid-card-stamp .gcs-sheen.wet {
    animation: gcStampSheenFade 640ms ease-out both;
    animation-delay: 300ms;
}
.grid-card-stamp .gcs-sheen.wet::before {
    animation: gcStampSheenSweep 640ms cubic-bezier(0.33, 0, 0.2, 1) both;
    animation-delay: 300ms;
}
@keyframes gcStampSheenFade {
    0%   { opacity: 0; }
    16%  { opacity: 0.92; }    /* catches the light as it lands */
    55%  { opacity: 0.52; }    /* still glistening */
    100% { opacity: 0; }       /* dried to the matte carved mark */
}
@keyframes gcStampSheenSweep {
    0%   { transform: translateX(0)    rotate(22deg); }
    100% { transform: translateX(320%) rotate(22deg); }
}
/* cast shadow blooms hard at contact, then tucks to a resting object's quiet shadow */
.grid-card-stamp-shadow.casting {
    animation: gcStampCast 720ms cubic-bezier(0.16, 0.72, 0.30, 1) both;
}
@keyframes gcStampCast {
    0%   { opacity: 0;    transform: scale(0.55); }
    18%  { opacity: 0;    transform: scale(0.62); }
    30%  { opacity: 0.75; transform: scale(1.06); }
    55%  { opacity: 0.42; transform: scale(0.96); }
    100% { opacity: 0.30; transform: scale(0.94); }
}
/* 奥へ一寸 — the painting RECEIVES the weight and presses a little DEEPER into the
   wall (sink + a faint brightness dip = settling into shadow, like a piece set into
   a 床の間 alcove), then eases back to plane. */
.grid-card.stamp-receive .grid-card-back-image {
    animation: gcStampReceive 660ms cubic-bezier(0.26, 0, 0.24, 1) both;
}
@keyframes gcStampReceive {
    0%   { transform: translateY(0)     scale(1);     filter: brightness(1);     }
    30%  { transform: translateY(4px)   scale(0.990); filter: brightness(0.975); }   /* mass pushes in + back */
    48%  { transform: translateY(4.5px) scale(0.988); filter: brightness(0.97);  }   /* held deepest, in shadow */
    100% { transform: translateY(0)     scale(1);     filter: brightness(1);     }   /* slow, weighted return to plane */
}

/* ===== 差し込む光 + 紙の記憶 — the press's physical signature (every grade) ===== */
/* A soft raking light sweeps once across the print as the seal lands (a low gallery
   lamp catching a fresh print), and a faint pressed-in paper ridge blooms around the
   seal as if the washi received the stamp. One pass each, then gone. Both sit UNDER
   the seal (z 22) + its shadow (z 21) and clip to the print. */
.grid-card-stamp-light {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 20;
    opacity: 0;
    mix-blend-mode: soft-light;
    background: linear-gradient(102deg,
        transparent 32%,
        rgba(255, 247, 230, 0) 40%,
        rgba(255, 247, 230, 0.85) 50%,
        rgba(255, 247, 230, 0) 60%,
        transparent 68%);
    background-size: 240% 100%;
    background-position: 135% 0;
    will-change: opacity, background-position;
}
.grid-card-stamp-light.cool {   /* cooler + softer on dark paintings, never a flashlight */
    background: linear-gradient(102deg,
        transparent 32%,
        rgba(222, 234, 255, 0) 40%,
        rgba(222, 234, 255, 0.7) 50%,
        rgba(222, 234, 255, 0) 60%,
        transparent 68%);
    background-size: 240% 100%;
    background-position: 135% 0;
}
.grid-card-stamp-light.raking {
    animation: gcStampRake 620ms cubic-bezier(0.33, 0, 0.2, 1) both;
}
@keyframes gcStampRake {
    0%   { opacity: 0; background-position: 135% 0; }
    20%  { opacity: var(--rake-a, 0.9); }                 /* light appears as the seal lands */
    100% { opacity: 0; background-position: -35% 0; }     /* sweeps across, then off the edge */
}
.grid-card-stamp-ridge {
    position: absolute;
    width: 96px;
    height: 96px;
    margin: -48px 0 0 -48px;          /* centred on the seal's placed point */
    border-radius: 50%;
    pointer-events: none;
    z-index: 20;
    opacity: 0;
    mix-blend-mode: overlay;          /* a light/shadow emboss, reads as a pressed ridge */
    background: radial-gradient(closest-side,
        transparent 56%,
        rgba(255, 255, 255, 0.18) 65%,
        rgba(56, 48, 38, 0.12) 73%,
        transparent 82%);
    transform: scale(0.5);
    will-change: opacity, transform;
}
.grid-card-stamp-ridge.pressed {
    animation: gcStampRidge 700ms cubic-bezier(0.16, 0.72, 0.30, 1) both;
}
@keyframes gcStampRidge {
    0%   { opacity: 0;   transform: scale(0.50); }
    22%  { opacity: 0;   transform: scale(0.70); }   /* nothing until contact */
    40%  { opacity: 0.9; transform: scale(0.96); }   /* the ridge pushes up around the seal */
    100% { opacity: 0;   transform: scale(1.12); }   /* relaxes flat into the paper */
}

/* ===== 金継ぎ + 陽だまり — the graduation payoff (only on the first Learned crossing) ===== */
/* one gold seam draws itself across the seal's ring, like a crack mended with gold */
.grid-card-stamp .gcs-kintsugi {
    position: absolute;
    inset: 0;
    z-index: 5;                      /* above the chop (2), under the sheen pass */
    pointer-events: none;
    overflow: visible;
    opacity: 0;
}
.grid-card-stamp .gcs-kintsugi path {
    fill: none;
    stroke: #E8C36A;                 /* bright molten gold so it reads on warm/green */
    stroke-width: 2.3;
    stroke-linecap: round;
    stroke-linejoin: round;
    /* a tight dark contact edge (separates it from any background) + a soft gold
       glow (the molten sheen) — together the seam reads as real gold on the seal */
    filter:
        drop-shadow(0 0.6px 0.6px rgba(40, 26, 6, 0.8))
        drop-shadow(0 0 2.4px rgba(232, 195, 120, 0.75));
    stroke-dasharray: var(--seam-len, 90);
    stroke-dashoffset: var(--seam-len, 90);
}
.grid-card-stamp.golden .gcs-kintsugi { animation: gcGoldFade 1500ms ease both; animation-delay: 380ms; }
.grid-card-stamp.golden .gcs-kintsugi path { animation: gcGoldDraw 780ms cubic-bezier(0.5, 0, 0.2, 1) both; animation-delay: 440ms; }
@keyframes gcGoldFade { 0% { opacity: 0; } 12% { opacity: 1; } 100% { opacity: 1; } }   /* appears at contact, then holds (rides the seal out) */
@keyframes gcGoldDraw { to { stroke-dashoffset: 0; } }                                   /* the seam draws itself, like molten gold filling a crack */

/* the painting drifts into warm late-afternoon light, then eases back to rest */
.grid-card-stamp-warm {
    position: absolute;
    inset: 0;
    z-index: 18;                     /* under the rake light + seal */
    pointer-events: none;
    opacity: 0;
    mix-blend-mode: soft-light;
    background: radial-gradient(130% 100% at 70% 74%,
        rgba(255, 208, 130, 0.95) 0%,
        rgba(255, 198, 120, 0.55) 38%,
        rgba(255, 190, 116, 0.20) 70%,
        transparent 90%);
    will-change: opacity;
}
.grid-card-stamp-warm.glowing { animation: gcWarmBloom 3400ms ease both; animation-delay: 360ms; }
@keyframes gcWarmBloom {
    0%   { opacity: 0;    }
    22%  { opacity: 0.95; }   /* the sun moves in */
    55%  { opacity: 0.70; }   /* golden hour, held */
    100% { opacity: 0;    }   /* eases back to the painting's rest tone */
}

/* ephemeral (Browse) dissolve — when the card STAYS put, the mark reads for a beat
   then lifts + fades like ink absorbing into the paper, leaving the card to its
   quiet status line. (In a study session the card flies off with the seal instead.) */
.grid-card-stamp.fading { animation: gcStampDissolve 700ms cubic-bezier(0.4, 0, 0.6, 1) forwards; }
@keyframes gcStampDissolve {
    from { opacity: 1; }
    to   { opacity: 0; transform: rotate(var(--stamp-crook, 2deg)) translateY(-4px) scale(1.03); }
}
.grid-card-stamp-shadow.fading { animation: gcStampShadowDissolve 600ms ease forwards; }
@keyframes gcStampShadowDissolve { to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
    .grid-card-stamp.pressing,
    .grid-card-stamp .gcs-core.soaking,
    .grid-card-stamp .gcs-halo.soaking,
    .grid-card-stamp .gcs-sheen.wet,
    .grid-card-stamp .gcs-sheen.wet::before,
    .grid-card-stamp-shadow.casting,
    .grid-card.stamp-receive .grid-card-back-image { animation: none !important; animation-delay: 0s !important; }
    .grid-card-stamp { transform: rotate(var(--stamp-crook, 2deg)); filter: none; }
    /* static stamped state: crook + carved depth + a SET two-layer mark + a faint
       fixed sheen + a resting cast shadow — no descent / soak / lift */
    .grid-card-stamp .gcs-core { transform: scale(1); opacity: 0.58; }
    .grid-card-stamp .gcs-halo { transform: scale(1); opacity: 0.72; }
    .grid-card-stamp .gcs-sheen { opacity: 0.42; }
    .grid-card-stamp .gcs-sheen::before { transform: translateX(150%) rotate(22deg); }
    .grid-card-stamp-shadow { opacity: 0.28; transform: scale(0.94); }
    /* dissolve stays — but as a plain opacity fade (no motion) */
    .grid-card-stamp, .grid-card-stamp-shadow { transition: opacity 450ms ease; }
    .grid-card-stamp.fading, .grid-card-stamp-shadow.fading { animation: none !important; opacity: 0; }
}

/* =============================================================================
   FRONT-FACE STATUS INDICATOR (Desktop) — dot + left-edge earned border
   ============================================================================= */

/* Status dot — top-right, visible at rest, hides on hover as action buttons appear */
.grid-card-front::before {
    content: '';
    position: absolute;
    top: 18px;
    right: 18px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    opacity: 0;
    z-index: 6;
    pointer-events: none;
    transition: opacity 0.25s ease;
}

.grid-card.status-known .grid-card-front::before {
    background: var(--color-moss);
    opacity: 0.7;
}

.grid-card.status-learning .grid-card-front::before {
    background: var(--color-amber);
    opacity: 0.65;
}

/* Hide dot on hover — action buttons take over */
.grid-card:hover .grid-card-front::before {
    opacity: 0;
}

/* De-collide the due card's top strip: while a card is veiled (asking face),
   suppress the 'known' status dot — it spoils exactly the recall the veil is
   asking for, and frees the corner from a three-mark pileup (fold + dot + thread). */
.grid-card.is-veiled.flipped .grid-card-front::before,
.grid-cards:not(.cards-flipped) .grid-card.is-veiled:not(.flipped):not([data-veil-lifted="1"]) .grid-card-front::before {
    opacity: 0 !important;
}


/* Left-edge earned border — inset shadow follows card border-radius naturally */
.grid-card.status-known .grid-card-front {
    box-shadow: inset 3px 0 0 0 rgba(107, 127, 92, 0.45);
}

.grid-card.status-learning .grid-card-front {
    box-shadow: inset 3px 0 0 0 rgba(160, 120, 80, 0.4);
}

/* De-collide the earned left edge too: while a card is veiled (asking face),
   drop the moss/amber inset edge so the recall face carries ONLY the amber
   'due' thread — same reasoning as the dot suppression above. This also
   restores the base paper drop-shadow the status-known/learning rules had
   replaced, so the veiled 'coming back' card reads as lifted paper, not flat.
   Selector specificity (4 classes) beats .grid-card.status-known (3). */
.grid-card.is-veiled.flipped .grid-card-front,
.grid-cards:not(.cards-flipped) .grid-card.is-veiled:not(.flipped):not([data-veil-lifted="1"]) .grid-card-front {
    box-shadow:
        0 1px 3px rgba(44, 44, 44, 0.04),
        0 4px 10px -4px rgba(44, 44, 44, 0.06);
}

/* Sync-triggered status change — subtle settle animation */
@keyframes syncCardSettle {
    0% { opacity: 0.5; transform: translateY(2px); }
    100% { opacity: 1; transform: translateY(0); }
}
.grid-card.sync-status-changed .grid-card-front::before {
    animation: syncCardSettle 400ms ease-out;
}

/* =============================================================================
   BACK-FACE ENRICHED CONTENT (Phase 3)
   ============================================================================= */

/* Header: Japanese text + romanization stacked (reading below word) */
/* THE PLACARD — museum label beneath the painting, written straight on the
   stone: no surface, no chrome. JP + ruby leads, romaji whispers under it,
   the gloss answers. Fixed total placard height (--placard-h) keeps every
   hanging the same height, which is what lets the wall be one plain CSS grid
   with zero measurement. */
.grid-card-back-header {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    padding: 14px 4px 0;
    padding-right: 48px;  /* Clear space for audio button */
    gap: 1px;
    height: auto;
    min-height: 0;
    max-height: none;
    flex-shrink: 0;       /* never squeeze the text — the painting absorbs */
}

/* Japanese text on back face — dynamic scaling to preserve image space */
.grid-card-back-japanese {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: clamp(1.1rem, calc(10rem / var(--ja-chars, 5)), 1.55rem);
    font-weight: 600;
    color: var(--color-ink);
    line-height: 1.25;
    white-space: nowrap;
    font-feature-settings: 'palt' 1;  /* True JP kerning, matches calligraphy face */
}

/* Larger furigana for readability */
.grid-card-back-japanese rt {
    font-size: 0.65em;
}

/* ONE HEIGHT FOR THE WORD LINE (desktop polish round 3, finding 2a). Every
   tile holds the reading's seat, read or not. A headword with furigana grew
   its line by the ruby (31 to 51.8px at the 1.55rem ceiling) and the
   painting, the placard's shock absorber, paid for it, so in a row mixing 方
   with くださる the paintings stepped 19.8px while the words stayed level
   (1,144 of 1,666 headwords carry furigana; kanji mode keeps the same seat
   with the reading hidden, and stepped the gloss line 17px as well). Now the
   line is one height in every tile, the bare line plus --ruby-seat (3.25rem,
   set with the wall metrics on .grid-cards, where the placard grows by the
   same seat, so no painting is cropped to pay for it). The word stands on
   the seat's floor (align-content: end), the reading above it where there is
   one, so headers, paintings and gloss lines run level across any row, in
   both modes, and a shorter long-word size can only leave air. */
.grid-cards .grid-card .grid-card-back-japanese {
    min-height: calc(1.55rem * 1.25 + var(--ruby-seat, 0px));
    align-content: end;
}

/* Romanization on back face — below Japanese text. Amber: the header
   romaji joins the pronunciation layer ("amber = how it sounds"), matching
   the story asides, example romaji and furigana in the story panel. */
.grid-card-back-romanization {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 0.78rem;
    font-weight: 400;
    font-style: italic;
    letter-spacing: 0.035em;
    color: var(--color-amber-deep);
}

/* (The .grid-verb-dict / .grid-verb-masu / .grid-verb-te "play form" affordances
   were retired 2026-06-28: the verb dict headword now renders plain like the noun
   branch, with its page-gated kanji as .wp-kt doors. The kanji-door hover styling
   lives in the .grid-card-back-japanese .wp-kt rule above.) */

/* Back-inner wrapper — flex column so image shrinks to fit after header/content */
.grid-card-back-inner {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    overflow: hidden;
}

/* Embedded placard reveal (reading journey R2a): the back face's inner core
   hosted inside ANOTHER card's answer face (the readctx exercise — owner
   ruling 2026-07-05, reveals are the word card). The flip-face fill/clip
   rules don't apply there: flow naturally, keep the 3:2 print. */
.grid-card-back-inner--embed {
    height: auto;
    flex: 1 1 auto;    /* fill the host face; the painting absorbs the squeeze
                          (its own flex 0 1 auto) so the host's grade row never
                          gets pushed off the face — the real back's behavior */
    min-height: 0;
    overflow: hidden;
}

/* Back-face status buttons container */
.grid-card-back-actions {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-shrink: 0;
    opacity: 0;
    transition: opacity 0.25s ease;
}

.grid-card:hover .grid-card-back-actions,
.grid-card:focus-within .grid-card-back-actions {
    opacity: 1;
}


/* POS tag — hidden in grid view (detail reserved for Focus mode) */
.grid-card-back-pos {
    display: none;
}

/* Focus button — now visible on desktop back face (hover reveal) */

/* Audio button on back face — top-right corner */
/* 音 — the listen chip floats on the painting's top-right like a label on
   museum glass: washi disc, ink glyph, present only while the hand is near. */
.grid-card-back-audio {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 5;
    width: 34px;
    height: 34px;
    border: none;
    border-radius: 50%;
    background: rgba(250, 250, 247, 0.88);
    box-shadow: 0 1px 4px rgba(25, 22, 18, 0.18);
    color: var(--color-ink);
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 0.95rem;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    opacity: 0;
    transform: scale(0.92);
    transition: opacity 0.25s ease, transform 0.25s ease, background-color 0.2s ease, box-shadow 0.2s ease;
    -webkit-tap-highlight-color: transparent;
    padding: 0;
}

.grid-card-back-audio svg { width: 17px; height: 17px; display: block; }

/* At rest the chip is INVISIBLE (opacity 0) — it must not stay a live 34px
   tap target on the painting's corner (a touch there played audio from
   nowhere and swallowed the flip). pointer-events follows visibility; the
   button stays tabbable, and Tab's :focus-within wakes it like hover. */
.grid-card-back-audio { pointer-events: none; }
.grid-card:hover .grid-card-back-audio,
.grid-card:focus-within .grid-card-back-audio { pointer-events: auto; }

.grid-card:hover .grid-card-back-audio,
.grid-card:focus-within .grid-card-back-audio {
    opacity: 0.92;
    transform: scale(1);
    transition-delay: var(--wake-3);  /* actions beat */
}

.grid-card-back-audio:hover {
    opacity: 1 !important;
    background: rgba(250, 250, 247, 0.96);
    transition-delay: 0s;  /* direct hover feedback stays instant */
}

.grid-card-back-audio:active {
    transform: scale(0.92);
    transition-duration: 0.1s;
}

.grid-card-back-audio.playing {
    animation: audioPulse 1s ease-out;
}

/* 陽 (yō, "sun") — the room door on the painting, a quiet warm-brown
   character just left of the audio button; the first character of the room's
   name, 陽だまり, the mark on every door into it since desktop polish round 4
   (14 October 2026; it was 間 here until then). No glow: an amber sun-disc
   used to sit behind the glyph, but over the gold painting it read as
   warmth-on-warmth and out-shouted the speaker (audio is the primary act).
   Removed 2026-06-27 so the door sits as a sibling to the audio button —
   same weight, only a hair warmer. A faint warm lift on hover is the only
   wake. Rendered only when the word has a real room. */
.grid-card-room-seal {
    position: absolute;
    top: 12px;
    right: 52px;       /* 6px gap left of the 34px audio button at right:12px */
    z-index: 5;
    width: 34px;       /* matches .grid-card-back-audio */
    height: 34px;
    border: none;
    border-radius: 50%;
    background: rgba(250, 250, 247, 0.88);
    box-shadow: 0 1px 4px rgba(25, 22, 18, 0.18);
    cursor: pointer;
    display: grid;
    place-items: center;
    opacity: 0;
    transform: scale(0.92);
    /* what moves, and only that: `all` animated the keyboard ring, so on
       focus the seal vanished for 150 ms and its ring morphed for 600 ms
       (desktop polish round 4, 14 October 2026, finding 6) */
    transition: opacity 0.25s ease, transform 0.25s ease, background-color 0.2s ease, box-shadow 0.2s ease;
    -webkit-tap-highlight-color: transparent;
    padding: 0;
}

.grid-card-room-seal .grid-card-room-sun {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: transparent;   /* glow disc removed — door sits quiet beside audio */
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-weight: 600;
    font-size: 15px;
    line-height: 1;
    color: #6F5524;            /* warm-brown, matches the speaker's visual weight */
    transition: filter 0.35s ease;
}

.grid-card:hover .grid-card-room-seal,
.grid-card:focus-within .grid-card-room-seal {
    opacity: 0.9;
    transform: scale(1);
    transition-delay: var(--wake-3);  /* rides the same actions beat as audio */
}

.grid-card-room-seal:hover {
    opacity: 1 !important;
    background: rgba(250, 250, 247, 0.96);
    box-shadow: 0 2px 7px rgba(25, 22, 18, 0.22);
    transition-delay: 0s;
}

.grid-card-room-seal:hover .grid-card-room-sun {
    filter: brightness(1.07) saturate(1.18);
}

.grid-card-room-seal:active {
    transform: scale(0.92);
    transition-duration: 0.1s;
}

@media (max-width: 768px) {
    .grid-card-room-seal { display: none; }
}

/* Hide old mark buttons overlay — status buttons are on front face now */
.grid-card-actions {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
}

/* =============================================================================
   CARD BACK - CONTENT LAYOUT
   ============================================================================= */

/* Back content area - flex row: English + POS + audio */
.grid-card-back-content {
    flex: 0 0 30px;       /* Fixed gloss-line height — uniform across all cards */
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 12px;
    padding: 0 4px;
    text-align: left;
    position: static;
    z-index: 1;
    overflow: hidden;
}

/* Ensure mark buttons appear above content area */
.grid-card-back .grid-card-actions {
    z-index: 10;
}


/* Large English text on back - single line with unfurl scroll on hover */
.grid-card-english-large {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 0.92rem;
    font-weight: 450;
    color: rgba(44, 44, 44, 0.72);
    line-height: 1.4;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: left;
    letter-spacing: -0.005em;
    flex: 1;
    min-width: 0;         /* Allow flex child to shrink below content size */
}

/* Save-in-browse whisper: a saved word returns TOMORROW (graded-today words
   rest), so the revisit pill won't move yet — this chip says so at the moment
   of the save, where the eye already is. Auto-fades after ~2s. */
.grid-save-whisper {
    position: absolute;
    right: 14px;
    bottom: 54px;
    z-index: 6;
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 5px 10px;
    background: var(--color-card, #FAFAF7);
    border: 1px solid rgba(44, 44, 44, 0.10);
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(44, 44, 44, 0.05);
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 12px;
    color: var(--color-ink, #2C2C2C);
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.3s ease, transform 0.3s ease;
    pointer-events: none;
}

.grid-save-whisper .gsw-en {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    font-size: 11px;
    color: var(--color-stone, #57534e);
}

.grid-save-whisper.gsw-visible {
    opacity: 1;
    transform: translateY(0);
}

/* (The back-face ⛶ chip is retired — tapping the painting IS the expand;
   the 音 disc stands alone at the painting's top-right.) */

/* Inline Unfurl — horizontal scroll on hover for truncated text */
.unfurl-wrap {
    overflow: hidden;
    width: 100%;
    mask-image: linear-gradient(to right, black 88%, transparent 100%);
    -webkit-mask-image: linear-gradient(to right, black 88%, transparent 100%);
}

.unfurl-wrap.unfurling {
    mask-image: linear-gradient(to right, transparent 0%, black 3%, black 97%, transparent 100%);
    -webkit-mask-image: linear-gradient(to right, transparent 0%, black 3%, black 97%, transparent 100%);
}

/* --- Gloss hover prototype: WRAP — the truncated line opens to its full
   wrapped form on a small paper panel; nothing moves, the text just breathes
   open. Escapes the one-line clip via :has() on the clipping parent. --- */
.grid-card-english-large:has(.gloss-wrap-open) {
    overflow: visible;
    position: relative;
}

/* The fixed 52px footer row clips overflow — release it while the gloss
   panel is open so the wrapped text can breathe over the meta row */
.grid-card-back-content:has(.gloss-wrap-open) {
    overflow: visible;
    z-index: 9;
}

.unfurl-wrap.gloss-wrap-open {
    position: absolute;
    /* Anchor the panel's bottom at the gloss line and grow UPWARD over the
       image — never downward over the meta row, where it would cover the
       Save/Skip buttons and make marking impossible. */
    bottom: -7px;
    left: -10px;
    right: -10px;
    padding: 6px 10px 7px;
    background: var(--color-card);
    border-radius: 8px;
    border: 1px solid rgba(44, 44, 44, 0.05);
    box-shadow: 0 4px 14px rgba(44, 44, 44, 0.08);
    z-index: 8;
    mask-image: none;
    -webkit-mask-image: none;
    animation: glossWrapIn 0.22s var(--ease-out-expo);
}

.unfurl-wrap.gloss-wrap-open .unfurl-text {
    white-space: normal;
    display: block;
}

@keyframes glossWrapIn {
    from { opacity: 0; transform: translateY(2px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Exits faster than it enters (per the motion voice) — the panel keeps its
   open layout while fading so it doesn't snap out of existence. */
.unfurl-wrap.gloss-wrap-closing {
    animation: glossWrapOut 0.15s ease forwards;
}

@keyframes glossWrapOut {
    from { opacity: 1; }
    to   { opacity: 0; }
}

/* --- Gloss hover prototype: FADE — crossfade through the gloss one wrapped
   line at a time; word-boundary safe because the browser does the wrapping. --- */
.unfurl-wrap.gloss-fading {
    max-height: 1.55em;
    mask-image: none;
    -webkit-mask-image: none;
}

.unfurl-wrap.gloss-fading .unfurl-text {
    display: block;
    white-space: normal;
    transition: opacity 0.2s ease;
}

.unfurl-text {
    display: inline-block;
    white-space: nowrap;
}

/* POS tag - hidden in grid view (shown in Focus mode only) */
.grid-card-pos {
    display: none !important;
}

/* Mark buttons on back - hidden until hover, positioned individually */
.grid-card-actions {
    position: absolute;
    inset: 0;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    padding: 12px;
    opacity: 0;
    z-index: 20;
    pointer-events: none;
    transition: opacity var(--grid-transition-medium) var(--ease-out-expo);
}

/* Known button - top left */
.grid-card-actions .grid-card-mark-btn.known {
    align-self: flex-start;
}

/* Learning button - top right */
.grid-card-actions .grid-card-mark-btn.learning {
    align-self: flex-start;
}

.grid-card-actions .grid-card-mark-btn {
    transform: translateY(8px);
    pointer-events: auto;
    transition:
        transform var(--grid-transition-medium) var(--ease-out-back),
        box-shadow var(--grid-transition-fast) ease,
        background var(--grid-transition-fast) ease;
}

/* Show mark buttons on flipped card hover (desktop 3D flip) */
.grid-card.flipped:hover .grid-card-actions,
.grid-card.flipped:focus-within .grid-card-actions {
    opacity: 1;
    transition: opacity 0.12s ease-out;
}

.grid-card.flipped:hover .grid-card-actions .grid-card-mark-btn,
.grid-card.flipped:focus-within .grid-card-actions .grid-card-mark-btn {
    transform: translateY(0);
    transition-delay: 0.1s;
    transition-duration: 0.15s;
}

/* =============================================================================
   MOBILE STYLES (≤768px)
   ============================================================================= */

/* =============================================================================
   EMPTY STATE — 余白 (Yohaku) Minimalist
   ============================================================================= */

@keyframes emptyFadeIn {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.grid-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 50vh;
    text-align: center;
    padding: 64px 40px;
    animation: emptyFadeIn 0.8s ease-out;
    /* span the full track set so the message centres in the viewport
       rather than collapsing into the first column of the display:grid */
    grid-column: 1 / -1;
}

.grid-empty-icon {
    margin-bottom: 32px;
    color: #a8a29e;
    opacity: 0.7;
}

.grid-empty-icon svg {
    width: 48px;
    height: 48px;
}

.grid-empty-text {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 1.3rem;
    font-weight: 400;
    color: var(--color-stone);
    margin-bottom: 12px;
    letter-spacing: 0.02em;
}

.grid-empty-hint {
    font-family: 'Inter', sans-serif;
    font-size: 0.85rem;
    font-weight: 400;
    color: #a8a29e;
    max-width: 280px;
    line-height: 1.6;
}

/* Queue-aware empty: a quiet bilingual line above the message. On a finished
   review session this is the one earned kintsugi-gold accent (principle 4). */
.grid-empty-jp {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 1.5rem;
    font-weight: 400;
    color: var(--color-stone);
    margin-bottom: 14px;
    letter-spacing: 0.04em;
}

.grid-empty-done .grid-empty-jp {
    color: var(--color-gold, #C9A96E);
}

.grid-empty-done .grid-empty-text {
    color: #6b6862;
}

/* Coverage line (kanji session end): what the mastered kanji BUY — the honest
   payoff number under the day's tally. One quiet line, no meter bar. */
.grid-empty-coverage {
    font-family: 'Inter', sans-serif;
    font-size: 0.85rem;
    color: #a8a29e;
    letter-spacing: 0.01em;
}

/* 稽古 session-end payoff (both おつかれ moments — kanji path and the word
   今日 session): 1-2 real sentences the learner can fully read NOW, every
   kanji mastered. The quiet close of the session shape: warm-up → work →
   payoff. Ruby carries the reading; romaji + English ride below per house
   rule. Moss cap = the earned-ability family (same as the unlock echo). */
.grid-empty-payoff {
    margin-top: 26px;
    max-width: 460px;
}
.payoff-cap {
    font-family: 'Inter', sans-serif;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--color-moss, #6B7F5C) 62%, #6b6660);
    margin-bottom: 10px;
}
.payoff-sent { margin-bottom: 14px; }
.payoff-sent:last-child { margin-bottom: 0; }
.payoff-jp {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 1.12rem;
    line-height: 2.1;   /* headroom for the ruby readings */
    color: var(--color-ink, #2C2C2C);
}
.payoff-jp rt { font-size: 0.55em; color: #8a857e; }
.payoff-ro {
    font-family: 'Inter', sans-serif;
    font-size: 0.78rem;
    font-style: italic;
    color: #a8a29e;
    margin-top: 2px;
}
.payoff-en {
    font-family: 'Inter', sans-serif;
    font-size: 0.82rem;
    color: #6b6862;
    margin-top: 1px;
}

/* Next-step chips — ghost buttons, no fill, no shadow (cards rest). */
.grid-empty-chips {
    display: flex;
    gap: 12px;
    margin-top: 28px;
    flex-wrap: wrap;
    justify-content: center;
}

.grid-empty-chip,
.recovery-chip {
    font-family: 'Inter', sans-serif;
    font-size: 0.85rem;
    color: var(--color-stone, #57534e);
    background: transparent;
    box-shadow: none;
    border: 1px solid rgba(44, 44, 44, 0.14);
    border-radius: 999px;
    padding: 8px 18px;
    min-height: 38px;
    cursor: pointer;
    transition: border-color 0.2s var(--ease-brush, ease), color 0.2s var(--ease-brush, ease);
}

.grid-empty-chip:hover,
.recovery-chip:hover {
    border-color: rgba(61, 90, 128, 0.4);
    color: var(--color-accent, #3D5A80);
}

.grid-empty-chip:focus-visible,
.recovery-chip:focus-visible {
    outline: 2px solid var(--color-accent, #3D5A80);
    outline-offset: 2px;
    box-shadow: none;   /* the ring only, never base.css's halo (round 3, finding 10) */
}

/* Recovery actions join the room's voice: ghost ink in the gallery's
   3px-radius grammar (the default-web pill died with the panel's ledger
   dress, 2026-06-12). Copy untouched — RECOVERY_AXES stays the single
   wording source. */
.recovery-chip {
    font-size: 12px;
    border-radius: 3px;
    padding: 7px 14px;
    min-height: 0;
}
.recovery-chip:hover {
    color: var(--text-primary, #2C2C2C);
    border-color: rgba(44, 44, 44, 0.28);
}

/* Clear-all recovery chip — the safe fallback carries the same ink left bar
   every selected row in the panel wears. */
.recovery-chip-clear {
    color: var(--text-secondary, #57534e);
    border-color: rgba(44, 44, 44, 0.14);
    border-left: 3px solid rgba(44, 44, 44, 0.55);
}

/* Keep-going chip on the session-done state — the continue action leads,
   same quiet voice with a touch more ink (accent text, firmer border). */
.grid-empty-chip-go {
    color: var(--color-accent, #3D5A80);
    border-color: rgba(61, 90, 128, 0.32);
}

/* First-visit welcome variant */
.grid-empty-welcome {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 1.1rem;
    color: var(--color-romaji);
    margin-top: 16px;
    padding: 12px 20px;
    background: rgba(201, 169, 110, 0.06);
    border-radius: 12px;
    border: 1px solid rgba(201, 169, 110, 0.12);
}

/* First-visit whisper on the loaded grid — one quiet self-erasing line that
   spans the full width above the cards, no box, no chrome. */
.grid-first-visit {
    grid-column: 1 / -1;
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 4px 20px 10px;
    text-align: center;
    color: var(--color-text-secondary, #57534e);
    opacity: 0;
    transform: translateY(-2px);
    transition: opacity 0.6s ease, transform 0.6s ease;
}
.grid-first-visit.gfv-visible {
    opacity: 1;
    transform: translateY(0);
}
.grid-first-visit.gfv-leaving {
    opacity: 0;
}
.grid-first-visit .gfv-jp {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 1rem;
    color: var(--color-romaji, #2C2C2C);
}
.grid-first-visit .gfv-sep {
    color: #a8a29e;
}
.grid-first-visit .gfv-en {
    font-family: 'Inter', sans-serif;
    font-size: 0.85rem;
}
.grid-first-visit kbd {
    font-family: 'Inter', sans-serif;
    font-size: 0.78rem;
    padding: 1px 6px;
    border: 1px solid rgba(44, 44, 44, 0.14);
    border-radius: 4px;
    background: var(--color-card, #FAFAF7);
}

/* =============================================================================
   折り目 (Orime) — RETIRED 2026-06-25 (owner)
   The earned-aging marks (faint gold edge-warmth on studied cards, the fold
   crease, and the mastered gold page-fold corner) are removed — the owner does
   not want any permanent gold mark on learned cards. The data-orime attribute is
   still set in JS but now renders nothing. Learned/Resting state shows ONLY via
   the moss status line + dot; the gold hanko seal was already retired earlier.
   ============================================================================= */

/* =============================================================================
   END-OF-LIST FOOTER
   ============================================================================= */

.grid-end-footer {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 32px 16px;
}

.grid-end-top-btn {
    padding: 8px 20px;
    border-radius: var(--radius-pill, 999px);
    border: 1px solid rgba(44, 44, 44, 0.08);
    background: var(--card-surface, #FAFAF7);
    color: var(--text-secondary, #57534e);
    font-family: 'Inter', sans-serif;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}

.grid-end-top-btn:hover {
    background: rgba(140, 133, 120, 0.06);
    border-color: rgba(44, 44, 44, 0.12);
}

.grid-end-top-btn:active {
    transform: translateY(0) scale(0.97);
}

/* =============================================================================
   +MORE CARD — the 今日 session's 続ける continuation card
   (Header re-named 2026-08-16: it read "batch continuation in Learn/Review
   queue", and the Learn/Review queue is the retired status decks. The card
   itself is untouched — buildSessionMoreCard writes .grid-more-card
   .grid-session-more, and it is now the only thing wearing this dress.)
   ============================================================================= */

/* Finished-batch keep-going: the 続ける card stands alone, centered like one
   gallery slot, instead of a dead-end done-screen (owner 2026-06-17). */
.grid-cards.grid-keepgoing-solo {
    display: flex;
    justify-content: center;
    align-items: flex-start;
}
.grid-cards.grid-keepgoing-solo .grid-more-card {
    width: min(400px, 100%);
}

.grid-more-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    background: var(--card-surface, #FAFAF7);
    border-radius: var(--radius-lg);
    border: 1px solid rgba(44, 44, 44, 0.06);
    box-shadow: 0 1px 3px rgba(44, 44, 44, 0.03);
    cursor: pointer;
    transition: border-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease;
    animation: cardEntrance 0.6s var(--ease-out-expo) backwards;
    min-height: 200px;
    position: relative;
    overflow: hidden;
}

.grid-more-card:hover {
    border-color: rgba(61, 90, 128, 0.15);
    box-shadow: 0 2px 8px rgba(44, 44, 44, 0.05);
}

.grid-more-card:hover .grid-more-corner {
    width: 40px;
    height: 40px;
}

.grid-more-card:active {
    transform: translateY(0) scale(0.98);
}

/* Page-turn corner */
.grid-more-corner {
    position: absolute;
    top: 0;
    right: 0;
    width: 36px;
    height: 36px;
    background: linear-gradient(225deg, var(--body-bg, #F0EDE8) 50%, transparent 50%);
    border-bottom-left-radius: 6px;
    border-bottom: 1px solid rgba(44, 44, 44, 0.04);
    border-left: 1px solid rgba(44, 44, 44, 0.04);
    transition: width 0.25s ease, height 0.25s ease;
}

.grid-more-jp {
    font-family: 'Klee One', serif;
    font-size: 32px;
    color: var(--text-primary, #2C2C2C);
    letter-spacing: 0.04em;
}

.grid-more-jp rt {
    font-family: 'Noto Sans JP', sans-serif;
    font-size: 11px;
    color: var(--text-secondary, #57534e);
    font-weight: 400;
}

.grid-more-translation {
    font-family: 'Inter', sans-serif;
    font-size: 13px;
    color: var(--text-secondary, #57534e);
    opacity: 0.45;
    font-style: italic;
}

.grid-more-count {
    font-family: 'Inter', sans-serif;
    font-size: 13px;
    color: var(--text-secondary, #57534e);
    opacity: 0.6;
    margin-top: 4px;
}

/* (.grid-more-of — the Learn deck's honest "20 of 1,195" line — and the
   .grid-more-progress / .grid-more-progress-fill hairline bar RETIRED
   2026-08-16 with the status decks. All three were written by the DECK's
   "+N more" ghost card alone (#gridMoreCard, gated on window._activeQueue),
   which went with the second beam row; the 今日 session's 続ける card never
   carried a bar or an N-of-M line — deliberately, since it paces a session
   rather than paginating a pool. Not a hole to refill: a browse wall does
   not page, and a progress thread over today's work would be keeping score
   (owner: copy never keeps score). */

/* =============================================================================
   LOADING STATE - Enhanced Skeleton
   ============================================================================= */

.grid-card.loading {
    pointer-events: none;
    overflow: hidden;
}

.grid-card.loading::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(
        90deg,
        rgba(255, 255, 255, 0.08) 25%,
        rgba(255, 255, 255, 0.18) 50%,
        rgba(255, 255, 255, 0.08) 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.8s ease-in-out infinite;
}

.grid-card.loading .grid-card-image {
    background: rgba(148, 163, 184, 0.06);
}

.grid-card.loading .grid-card-content {
    background: transparent;
}

.grid-card.loading .grid-card-japanese,
.grid-card.loading .grid-card-romanization {
    background: rgba(148, 163, 184, 0.12);
    border-radius: 6px;
    color: transparent !important;
    min-height: 1.5em;
}

/* Status indication via card-level classes and status bar */

/* =============================================================================
   ACTION BAR - HIDE IN GRID MODE
   ============================================================================= */

/* Hide the entire action bar in grid mode */
body.grid-mode .bottom-action-bar {
    display: none !important;
}

/* =============================================================================
   PREMIUM POLISH - DESKTOP ENHANCEMENTS
   Only apply scroll-aware animations on desktop (causes issues on mobile)
   ============================================================================= */

@media (min-width: 769px) {
    /* 1. Scroll-aware card entrance - cards animate when first scrolling into view */
    .grid-card:not(.in-view) {
        opacity: 0;
        transform: translateY(12px);
        animation: none;
    }

    .grid-card.in-view {
        animation: cardEntrance 0.6s var(--ease-out-expo) forwards;
    }

    /* The hang, then the label dries: inside each card's entrance the painting
       settles first; the placard's ink resolves a beat later (blur is brief —
       longer reads as broken rendering). Composes with the card-level stagger:
       both children inherit the card's own delay through `backwards` fill. */
    .grid-card.in-view .grid-card-back-image {
        animation: paintingHang 0.55s var(--ease-out-expo) backwards;
    }
    .grid-card.in-view .grid-card-back-header,
    .grid-card.in-view .grid-card-back-content {
        animation: placardDry 0.45s ease-out 0.22s backwards;
    }
}

/* Refinement renders (search keystrokes, the 今日 session's 続ける top-up,
   a post-grade re-render): the world stays put — no child cascade replay.
   (Read "+N more, deck re-sync" until 2026-08-16 — both of those callers were
   the status decks', and both went with them.) */
.grid-cards.no-entrance .grid-card .grid-card-back-image,
.grid-cards.no-entrance .grid-card .grid-card-back-header,
.grid-cards.no-entrance .grid-card .grid-card-back-content {
    animation: none !important;
}

@keyframes paintingHang {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes placardDry {
    from {
        opacity: 0;
        filter: blur(2.5px);
    }
    to {
        opacity: 1;
        filter: blur(0);
    }
}

/* Focus button — no ambient glow (和の感性: scarcity gives meaning) */

/* 3. English text reveal animation on back face */
@keyframes textReveal {
    from {
        opacity: 0;
        transform: translateY(10px);
        filter: blur(4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
        filter: blur(0);
    }
}

.grid-card.flipped .grid-card-english-large {
    animation: textReveal 0.4s ease-out 0.15s backwards;
}

/* =============================================================================
   PREMIUM POLISH - AUDIO PLAYING EFFECTS
   (.grid-card-audio sound-wave ring suite deleted — it styled a ghost; the
   live buttons are .grid-card-front-audio / .grid-card-back-audio)
   ============================================================================= */

/* Japanese text glow - use transform for GPU acceleration */
.grid-card.audio-playing .grid-card-japanese {
    text-shadow:
        0 0 8px rgba(61, 90, 128, 0.24),
        0 0 16px rgba(61, 90, 128, 0.12);
    transition: text-shadow 0.3s ease-out;
}

/* Fade out: smooth return to resting ink diffusion */
.grid-card.audio-fade-out .grid-card-japanese,
.grid-card:not(.audio-playing) .grid-card-japanese {
    text-shadow: 0 0 0.5px rgba(44, 44, 44, 0.12);  /* Resting ink absorption */
    transition: text-shadow 0.3s ease-out;
}

/* Card glow effect (desktop only) - static glow, not animated */
@media (min-width: 769px) {
    .grid-card.audio-playing {
        box-shadow:
            var(--grid-card-shadow),
            0 0 25px rgba(61, 90, 128, 0.09);
        transition: box-shadow 0.3s ease-out;
    }

    .grid-card.audio-fade-out,
    .grid-card:not(.audio-playing) {
        transition: box-shadow 0.3s ease-out;
    }
}

/* =============================================================================
   PREMIUM POLISH - MOBILE ENHANCEMENTS
   ============================================================================= */

/* =============================================================================
   REDUCED MOTION - Grid-specific accessibility
   Replaces 3D flip with opacity crossfade, disables accordion animation
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
    /* Disable 3D flip — use opacity crossfade instead */
    .grid-card {
        perspective: none;
        /* Base entrance wave off (desktop .in-view path covered below) */
        animation: none;
        /* Hover-cascade reveals become instant */
        --wake-2: 0ms;
        --wake-3: 0ms;
    }

    /* Still the entrance/exit/shimmer animations (card removal is covered
       by the JS timer fallback, which doesn't rely on animationend) */
    .grid-card.in-view,
    .grid-card.exiting,
    .grid-card.exiting-lift {
        animation: none !important;
    }

    /* Gallery wall stillness: no hang/dry cascade, no painting breathe,
       no feature entrance — the wall is simply there. */
    .grid-card.in-view .grid-card-back-image,
    .grid-card.in-view .grid-card-back-header,
    .grid-card.in-view .grid-card-back-content,
    .grid-feature {
        animation: none !important;
    }

    .grid-card-back-image img,
    .gf-art img {
        transition: none !important;
        transform: none !important;
    }

    .grid-card-verbline,
    .grid-card-hanko {
        transition: none;
    }

    .grid-card-echo,
    .grid-card-echo.echo-expired,
    .grid-cards.masonry-layout.masonry-settled .grid-card-echo,
    .grid-card-echo .grid-echo-word,
    .grid-save-whisper {
        transition: none;
        transform: none;
        filter: none;
    }

    .grid-card-back-image.img-loading,
    .grid-card-image.img-loading {
        animation: none;
    }

    .grid-card-inner {
        transform-style: flat;
        transition: none;
    }

    .grid-card.flipped .grid-card-inner {
        transform: none;
    }

    .grid-card-front,
    .grid-card-back {
        backface-visibility: visible;
        transform: none;
    }

    .grid-card-front {
        opacity: 1;
        transition: opacity 0.01ms;
    }

    .grid-card.flipped .grid-card-front {
        opacity: 0;
    }

    .grid-card-back {
        opacity: 0;
        transform: none;
    }

    .grid-card.flipped .grid-card-back {
        opacity: 1;
    }

    /* Disable accordion expand/collapse animation (mobile) */
    .grid-card.expanded {
        transition: none;
    }

    .grid-card-back {
        transition: none !important;
    }

    /* Disable hover lift */
    .grid-card:hover {
        transform: none;
    }

}

/* =============================================================================
   WORD STORY SHOJI PANEL
   ============================================================================= */

/* (.grid-card-wordlink ↗ "Open" anchor retired in round 4 — the open-as-URL
   idea is gone; related words open the in-app word page instead.) */

/* Deep-link "outside your filters" notice — one quiet row above the grid */
.deep-link-notice {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 0 0 16px;
    padding: 10px 16px;
    border: 1px solid rgba(61, 90, 128, 0.18);
    border-radius: 10px;
    background: var(--color-card-bright, #FEFDFB);
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13px;
    color: var(--text-secondary, #57534e);
}
.deep-link-notice .dln-action {
    border: none;
    background: transparent;
    box-shadow: none;
    padding: 2px 4px;
    font: inherit;
    color: var(--color-accent, #3D5A80);
    cursor: pointer;
}
.deep-link-notice .dln-action:hover {
    text-decoration: underline;
    box-shadow: none;
}
.deep-link-notice .dln-sep { color: var(--text-tertiary, #8C8578); }

/* ── Orphaned story-sheet content styles ─────────────────────────────────
   Nothing renders the blocks below any more — the story handles went
   2026-06-11 and the sheet they lived on went with the 活用 handle. They are
   left standing because sidebar.css and video-mode.css cite .grid-story-text
   as the reference for their own prose scale; retire them together with those
   two, not before. */

/* Section text — readable body.
   Font stack: Inter carries the Latin prose; JP glyphs (which Inter lacks)
   fall through to Klee One — the same face as the headword and the example
   sentences. Without Klee here, inline JP fell to Noto Sans JP (gothic) and
   the card mixed two Japanese typefaces. */
.grid-story-text {
    font-family: 'Inter', 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 15px;
    line-height: 1.6;
    color: #44403c;
}

.grid-story-text strong {
    color: var(--color-ink);
    font-weight: 600;
}

/* Inline Japanese inside story prose — JP glyphs are denser than Latin at the
   same px size, so give them a step up to read at parity with the body.
   Weight 600 = Klee One's loaded emphasis weight (the global .kanji-term 500
   would synthesize); matches the studied-word emphasis used in examples. */
.grid-story-text .kanji-term {
    font-size: 1.15em;
    font-weight: 600;
}

/* Example sentences (orphan — see the note above) */
.grid-story-example {
    padding: 8px 0;
}

.grid-story-example + .grid-story-example {
    border-top: 1px solid rgba(44, 44, 44, 0.03);
}

/* The Japanese sentence is what the learner actually studies — it leads the
   example block, a clear step above the romaji/English support lines. */
.grid-story-example-jp {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 18px;
    line-height: 1.7;
    color: var(--color-ink);
}

/* Furigana in example sentences — the global ruby rt ratio (0.4em, base.css)
   is tuned for large headwords; on a body-size line it lands unreadably small.
   Scale with the sentence (≈11.5px at 18px), floored at 10px. Amber: furigana
   joins the pronunciation layer — ONE learnable rule, "amber = how it sounds"
   (furigana above the kanji + the romaji line below the sentence). */
.grid-story-example-jp ruby rt {
    font-size: max(0.64em, 10px);
    color: var(--color-amber-deep);
    opacity: 1;
}

.grid-story-example-romaji {
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--color-amber-deep);
    margin-top: 2px;
    font-style: italic;
}

.grid-story-example-en {
    font-size: 14px;
    line-height: 1.5;
    color: var(--color-ink);
    margin-top: 1px;
}

/* Counter list (orphan — see the note above; compact, collapsible) */
.grid-counter-list {
    display: flex;
    flex-direction: column;
}
.grid-counter-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 2px;
    border-radius: 6px;
}
.grid-counter-num {
    font-size: 10px;
    font-weight: 500;
    color: #a8a29e;
    opacity: 0.5;
    min-width: 16px;
    text-align: right;
    flex-shrink: 0;
}
.grid-counter-jp {
    font-family: 'Klee One', 'Noto Sans JP', sans-serif;
    font-size: 15px;
    font-weight: 600;
    color: var(--color-ink);
    line-height: 1.4;
    letter-spacing: 0.02em;
    flex: 1;
    min-width: 0;
}
.grid-counter-jp rt {
    font-size: max(0.5em, 8px);
    color: #a8a29e;
    font-weight: 400;
    letter-spacing: 0.05em;
}
.grid-counter-reading {
    font-family: Inter, sans-serif;
    font-size: 12px;
    font-style: italic;
    color: #a8a29e;
    letter-spacing: 0.02em;
    flex-shrink: 0;
}
.grid-counter-audio-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    border: 1px solid rgba(44, 44, 44, 0.06);
    border-radius: 50%;
    background: transparent;
    color: #a8a29e;
    opacity: 0.4;
    cursor: pointer;
    padding: 0;
}
.grid-counter-audio-btn:active,
.grid-counter-audio-btn.playing { opacity: 1; color: var(--color-gold); }
.grid-counter-question {
    border-top: 1px solid rgba(201, 169, 110, 0.15);
    margin-top: 2px;
    padding-top: 6px;
}
.grid-counter-question .grid-counter-num {
    color: var(--color-gold);
    opacity: 0.7;
    font-weight: 600;
}
.grid-counter-question .grid-counter-reading {
    color: var(--color-gold);
    opacity: 0.6;
}
/* Collapse/expand */
.grid-counter-more {
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    transition: max-height 0.25s ease-out, opacity 0.2s ease-out;
}
.grid-counter-more.expanded {
    max-height: 600px;
    opacity: 1;
}
.grid-counter-toggle {
    padding: 6px 2px;
    cursor: pointer;
    text-align: center;
}
.grid-counter-toggle-text {
    font-family: Inter, sans-serif;
    font-size: 11px;
    font-style: italic;
    color: #a8a29e;
    opacity: 0.6;
    transition: opacity 0.15s ease;
}
.grid-counter-toggle:hover .grid-counter-toggle-text {
    opacity: 1;
}


/* =============================================================================
   WORD RELATIONSHIPS — In-place Drawer (和の感性)
   The card's inline "N related" chip is retired, and the 活用 chip that
   outlived it on the meta row has followed — css/freq-tier.css carries
   neither now. The group/row styles below still dress relation lists on the
   surfaces js/sentences.js renders.
   ============================================================================= */

/* ---------- Related Words — Slide-out Panel Below Card ---------- */

/* Group label — uppercase, small — always visible */
.rel-group-label {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-tertiary, #8C8578);
    margin-bottom: 6px;
    padding-left: 2px;
}

/* Divider between groups */
.rel-divider {
    height: 1px;
    background: rgba(44, 44, 44, 0.04);
    margin: 10px 4px;
}

/* Hide first divider */
.rel-group:first-child + .rel-divider {
    /* keep visible */
}

/* Word row — two-row layout: dot + body */
.rel-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 8px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 150ms ease;
}

.rel-row:hover {
    background: rgba(44, 44, 44, 0.025);
}

/* (.rel-row-link ↗ open-in-tab anchor retired in round 4 — clicking the row now
   opens the in-app word page directly, no separate word URL.) */

/* Colored dot — vertically centered against the two rows */
.rel-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    flex-shrink: 0;
}

.rel-dot--synonym   { background: var(--rel-similar); }
.rel-dot--antonym   { background: var(--rel-opposite); }
.rel-dot--homophone { background: var(--rel-sound); }
.rel-dot--homograph { background: var(--rel-writing); }
.rel-dot--related   { background: var(--rel-loose); } /* was --color-accent — loose ties must not wear interactive indigo */

/* Body wraps both rows, fills remaining width */
.rel-row-body {
    flex: 1;
    min-width: 0;
}

/* Top row: kanji (left) + english (right) */
.rel-row-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

.rel-row-kanji {
    font-family: 'Klee One', serif;
    font-size: 22px;
    font-weight: 600;
    color: var(--text-primary, #2C2C2C);
    line-height: 1.2;
}

.rel-row-en {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 13px;
    color: var(--text-secondary, #57534e);
    text-align: right;
    flex-shrink: 0;
    margin-left: auto;
}

/* Audio button — appears on hover, sits between kanji and english */
.rel-row-audio {
    border: none;
    background: none;
    box-shadow: none;
    padding: 2px 6px;
    cursor: pointer;
    font-size: 10px;
    color: var(--text-tertiary, #8C8578);
    opacity: 0;
    transition: opacity 200ms ease, color 200ms ease;
    flex-shrink: 0;
}

.rel-row:hover .rel-row-audio {
    opacity: 0.6;
}

.rel-row-audio:hover {
    opacity: 1 !important;
    color: var(--text-primary, #2C2C2C);
    transform: none;
    box-shadow: none;
}

.rel-row-audio:active {
    transform: none;
}

/* Sub row: kana · romanization */
.rel-row-sub {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 12px;
    color: var(--text-tertiary, #8C8578);
    margin-top: 2px;
    letter-spacing: 0.01em;
}

/* Highlight flash when navigating to a related card */
@keyframes relHighlight {
    0% { box-shadow: 0 0 0 3px rgba(201, 169, 110, 0.5); }
    100% { box-shadow: 0 0 0 3px rgba(201, 169, 110, 0); }
}

.grid-card.rel-highlight {
    animation: relHighlight 1.5s ease-out;
}

/* (The in-grid related drawer + its 凡例 .rdx-* styles were retired in
   owner round 5 — related words live solely on the word page now. The
   .rel-row / .rel-dot / .rel-group-label rules ABOVE are legacy: Focus
   mode (js/sentences.js renderRelatedWordsSection) still emits them —
   keep until that surface migrates. Their swatches now draw from the
   shared --rel-* tokens (docs/SURFACE_CONTRACT.md), so legacy reads the
   same legend as everything else. .rel-highlight stays: the deep-link
   follow flashes its landing card.) */

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
    .grid-card.rel-highlight {
        animation: none;
        box-shadow: 0 0 0 3px rgba(201, 169, 110, 0.3);
    }
}

/* ── ONE RING (desktop polish round 3, finding 10) ───────────────────────────
   Every control on the wall shows keyboard focus the one way the beam, the
   filter panel and the palette do: a 2px indigo outline 2px off its edge, and
   nothing else. base.css:309-318 lays a 4px 15% halo under every focused
   button, [role=button] and .grid-card app-wide, so it stays there and the
   wall takes it off here; Today's piece also drops its own faint 1.5px ring.
   A control keeps its resting dress while focused: the 音 and 陽 discs keep
   their seat on the painting, the 続ける card its hairline seat, the Back to
   top pill the app's resting button shadow (components.css), so focus adds
   the ring and only the ring. (The kanji card's ring lives in
   kanji-grid.css; the seam words' and the empty wall's chips sit by their
   own rules above. A tile mid-flash from a related-word jump keeps its
   gold.) */
.grid-cards .grid-card:not(.grid-card--kanji):not(.rel-highlight):focus-visible,
.grid-container .grid-feature:focus-visible,
.grid-container .gf-audio:focus-visible,
.grid-cards .grid-card .grid-card-status-btn:focus-visible,
.grid-cards .grid-card .grid-card-front-audio:focus-visible {
    outline: 2px solid var(--color-accent, #3D5A80);
    outline-offset: 2px;
    box-shadow: none;
}
.grid-container .grid-feature:focus-visible { border-radius: 0; }
.grid-cards .grid-card .grid-card-back-audio:focus-visible,
.grid-cards .grid-card .grid-card-room-seal:focus-visible {
    outline: 2px solid var(--color-accent, #3D5A80);
    outline-offset: 2px;
    box-shadow: 0 1px 4px rgba(25, 22, 18, 0.18);
}
/* …and the keyboard sees the disc it reached at once (round 4, finding 6):
   the tile's wake rides :focus-within on the 140 ms actions beat, a pointer's
   courtesy; a focused control owes no wait, no fade and no scale-in under its
   ring. Only the arrival is cut: leaving focus takes the resting rules'
   transition, so the disc still settles back on the tile's own beat. */
.grid-cards .grid-card .grid-card-room-seal:focus-visible,
.grid-cards .grid-card .grid-card-back-audio:focus-visible {
    opacity: 1;
    transform: none;
    transition: none;
}
.grid-more-card:focus-visible {
    outline: 2px solid var(--color-accent, #3D5A80);
    outline-offset: 2px;
    box-shadow: 0 1px 3px rgba(44, 44, 44, 0.03);
}
.grid-end-top-btn:focus-visible {
    outline: 2px solid var(--color-accent, #3D5A80);
    outline-offset: 2px;
    box-shadow: 0 4px 15px rgba(44, 44, 44, 0.06);
}

/* …and the reading wakes for the keyboard as it wakes for the pointer (the
   three doors: hover, :focus-visible, tap). A focused tile already shows its
   meta and its verbs (they ride :focus-within); its romaji stayed hidden and
   a blank line sat where "iu" would be. Mode for mode the same wake as the
   hover rule at the foot of this file: the furigana in kanji mode, the romaji
   otherwise. On a pointer that hovers, the reading follows :focus-within
   exactly like the verbs, so a tile handed focus back after its page closes
   never shows the verbs over a blank line. On a touch screen only the
   keyboard's own focus wakes it, never the quiet focus a closing page hands
   back after a tap (Chrome marks that script focus :focus-visible too), so a
   finger prints no romaji (the touch door is the tap to the word page; the
   romaji ruling below). */
:root[data-rmode="kanji"] .grid-card:not(.quiet-focus):is(:focus-visible, :has(:focus-visible)) .grid-card-back-japanese rt,
:root[data-rmode="kanji"] .grid-card:not(.quiet-focus):is(:focus-visible, :has(:focus-visible)) .grid-card-japanese rt,
:root:not([data-rmode="kanji"]) .grid-card:not(.quiet-focus):is(:focus-visible, :has(:focus-visible)) .grid-card-back-romanization {
    visibility: visible;
}
@media (hover: hover) and (pointer: fine) {
    :root[data-rmode="kanji"] .grid-card:focus-within .grid-card-back-japanese rt,
    :root[data-rmode="kanji"] .grid-card:focus-within .grid-card-japanese rt,
    :root:not([data-rmode="kanji"]) .grid-card:focus-within .grid-card-back-romanization {
        visibility: visible;
    }
}

/* Programmatic focus restores (word-page open/close handing focus back)
   suppress the keyboard ring — Chrome paints :focus-visible on script
   focus after any keypress (the "blue border" bug). Real Tab navigation
   still shows rings: the class clears on blur. */
.quiet-focus:focus-visible,
.quiet-focus:focus {
    outline: none !important;
}

/* ═══════════ TOUCH — the one visibility law's no-hover clause ═══════════
   (owner 2026-07-22) On hover-capable pointers the grade pair rides the
   hover reveal on the browse wall (今日 stays framed; the status decks were
   the third surface here until they retired 2026-08-16). A
   finger has no hover, so on touch screens the pair is simply always there
   — iPad runs the desktop product and a hover-only control would be
   unreachable. The buttons wear their woken frame permanently: the 5px
   rest dot is not a finger target. Selectors carry one extra ancestor so
   they outrank the rest-state rules regardless of file order (freq-tier.css
   loads after this file). The due-veil recall gate (visibility:hidden on
   unlifted asks) is a different property and still wins. */
@media (hover: none) {
    .grid-cards .grid-card .grid-card-meta-row {
        opacity: 1;
        pointer-events: auto;
    }
    /* The IDENTITY strip (noun · EVERYDAY · the rank tail) was hover-only on
       every device, including iPad — this block already rescued the grade
       pair and the statusline but never it. So on the owner's primary device
       the word's part of speech and how common it is were both unreachable,
       and after the 地の文 ruling (2026-08-01) the exact rank rides this same
       line. A finger has no hover: on touch the strip and its tail are simply
       always there. Same one-extra-ancestor trick as the rules above (0,3,0
       over freq-tier.css's 0,1,0) so file order stays irrelevant. */
    .grid-cards .grid-card .grid-card-meta-top {
        opacity: 1;
        pointer-events: auto;
    }
    .grid-cards .grid-card .grid-card-meta-top .meta-freq-tail {
        opacity: 1;
        transition: none;
    }
    /* (the touch rescue for the status decks' hover-gated centre cell went
       with the decks, 2026-08-16 — .deck-live is unsettable.) */
    /* The statusline is the SOLE state display (the receipt law: .active is a
       press receipt, never a state). It slept at opacity:0 and woke only on
       :hover / :focus-within — with no escape here, so on touch the learner
       got the two verbs above and never the word saying where the card stands.
       Same one-extra-ancestor trick as the meta-row rule (0,3,0 over the base
       rule's 0,1,0), so file order stays irrelevant. `.state-new { display:
       none }` is a different property and still wins: an untouched word has no
       journey yet and stays silent. */
    .grid-cards .grid-card .grid-card-statusline {
        opacity: 1;
    }
}

/* ── 表示 kanji mode: the reading rests, a pointer wakes it ───────────────────
   Owner ruling 2026-08-06 (round 2): "in kanji mode hover over the card in
   browse". The mode decides what is PRINTED — the kanji alone — and a rest of
   the pointer adds what it holds back: the READING, and never romaji. A reader
   who chose kanji has chosen native text; latin is the cheaper road the whole
   romaji ruling exists to close.

   ⛔ WHAT WAKES DEPENDS ON THE MODE, because the modes hold back different
   things:
     kanji     the FURIGANA over the characters — and NEVER romaji. A reader who
               chose kanji has chosen native text; latin is the cheaper road the
               whole romaji ruling exists to close.
     furigana  the romaji (the ruby is already up).
     kana      the romaji (the text IS the reading).

   ⛔ A separate hiragana ROW was built for kanji mode and cut by the owner:
   with the ruby up, 歩く already reads あるく — the ruby says ある and the く is
   right there in the headword. Two ways of saying one word is the thing this
   app's own rule forbids.

   ⛔ Guarded on real hover. A touch device gets no rest of a pointer, so none of
   this applies there and the card's own tap still travels to the word page,
   which carries the same offer on a real control.

   ⛔⛔ THE SEAT IS PERMANENT; ONLY THE INK COMES AND GOES (2026-08-16).
   This block first shipped as `display: none` → `display: ruby-text`, reasoning
   that a merely-faded annotation still RESERVES its line box. True — and that
   reservation is the POINT, because the alternative is not "no cost" but "the
   cost paid as motion, at the moment the reader is looking". Measured on the
   built wall (probe: rest box vs hovered box, 1680×1050):

     kanji mode    the headword DROPS 17.0px and the painting loses 3.8px
     furigana mode the headword RISES 16.0px and the painting loses 16.0px

   The card is `--painting-h + --placard-h`, fixed, and `.grid-card-back-image`
   is `flex: 0 1 auto` — declared the placard's "shock absorber". A row that
   appears on hover therefore does not merely push text: it RE-CROPS THE
   ARTWORK under the pointer. That is the jump the owner reported ("the kanji
   jumps/shakes instead of the furigana quietly being displayed above").

   So the reading keeps its seat at rest and hover changes only whether there is
   ink in it. That is not a new idea here — it is the mechanism the walk's own
   withheld readings already use (the ask withholds the reading; ink returns
   without moving the seat), it is the standing display law of the B4 veil (css/base.css:
   "Transparent ink, NOT display:none — layout identical"), and it is what THIS
   CARD already does for the rest of its hover chrome: the 50px grade-verb row
   and the meta row hold their boxes at rest and only ink in under the pointer.
   `visibility`, not opacity: the global `ruby rt` sets opacity .55 as the
   resting alpha of a SHOWN reading, and overriding it here would make the woken
   reading come back at the wrong strength.

   ⚖️ WHAT THE SEAT COSTS, stated plainly so it can be overruled with the eye:
   the print pays it, statically, in the two places the placard has no slack
   left — and paying it statically is what css:1138 ALREADY sanctions ("the
   painting is the placard's shock absorber… the PAINTING gives up the few px —
   object-fit crops invisibly"). kanji mode: −3.8px of print, headword one band
   lower, which reads as air under the picture. furigana/kana: −16px of print
   and a blank line between headword and gloss where the romaji will land. If
   the blank line reads as a hole rather than a rest, the fixes are (a) raise
   --placard-h by 16px so the CARD grows instead of the print shrinking, or (b)
   drop the wall's romaji-on-hover, which the word page offers on a real
   control. What is NOT on the menu is paying it as motion.

   ⛔ The one row that keeps NO seat is the romaji in kanji mode — nothing there
   can ever wake it, and a seat for a row that can never fill IS the hole the
   first version rightly refused. */
/* ⛔ BOTH FACES. The gallery face (.grid-card-back-japanese) is the wall at
   rest; the RECALL face (.grid-card-japanese) is the one a flip turns up for
   review, and in kanji mode that face must ask its question in KANJI — a
   furigana'd 虫 hands over the answer before the reader has tried. Missing it
   was a real regression: the ruby is built once and feeds both faces, so
   teaching one to hide it left the other showing it. The hint is a hover away
   on both, for a reader who is stuck. */
:root[data-rmode="kanji"] .grid-card .grid-card-back-japanese rt,
:root[data-rmode="kanji"] .grid-card .grid-card-japanese rt,
.grid-card .grid-card-back-romanization { visibility: hidden; }

/* kanji mode holds the romaji back FOREVER, so it keeps no seat there */
:root[data-rmode="kanji"] .grid-card .grid-card-back-romanization { display: none; }

@media (hover: hover) and (pointer: fine) {
    :root[data-rmode="kanji"] .grid-card:hover .grid-card-back-japanese rt,
    :root[data-rmode="kanji"] .grid-card:hover .grid-card-japanese rt,
    /* ⛔ NOT in kanji mode — there the reveal is the furigana, and romaji would
       hand a native-text reader the latin they chose to stop using. */
    :root:not([data-rmode="kanji"]) .grid-card:hover .grid-card-back-romanization {
        visibility: visible;
    }
}

/* ⭐ U-100 (the deep review, Round 3; the owner's YES of 26 October 2026, rows 3
   and 3b): THE COURSE CARD'S TEST FACE ASKS IN KANJI. On the course's 単語帳
   Flashcards (body.course-shelf-active) the test face (.grid-card-front, the
   face 裏 Flip and 隠す Hide turn up) holds its ruby back, its seat kept (the
   idiom above), whatever 表示 says and under the pointer or the keyboard's focus
   too (the selector outranks both wakes): a turned 先生 asks its meaning with no
   せんせい over it, so the kanji is what is read (rule 37: nothing answers before
   the attempt). The press that turns it back shows the answer face with its
   kana; the picture face, the wall at rest, keeps its kana; 表示 keeps governing
   both, untouched. */
body.course-shelf-active .grid-cards .grid-card.grid-card--cw .grid-card-front .grid-card-japanese rt { visibility: hidden; }
