/* ===========================================================================
   Pool Practice - design tokens
   ---------------------------------------------------------------------------
   One source for colour, type, spacing and the geometry of the action rail.
   Every page that has been through the redesign reads from here; nothing in a
   page should invent a colour or a size of its own.

   THE ROOM. Brian lights the table hard enough to film at 960 fps. A glossy
   iPad in that room is a mirror, so a large black field is the worst possible
   ground: it shows the ceiling, the lamps and the player. Reading surfaces are
   therefore light (--chalk) and the ground is a mid-tone that survives glare
   (--cloth).

   THE ONE RULE. **Text never sits on cloth.** Cloth is the ground - the thing
   objects lie on. Measured: chalk on cloth is 3.48:1 and ink on cloth 4.35:1,
   both under the 4.5:1 body-text line, and the first draft of this palette was
   "fixed" by darkening the green until it stopped being cloth. Giving every
   text run a surface is the fix that keeps the green.

   Measured contrast, sRGB, computed not guessed:

     ink   on chalk   15.14      chalk on deep    8.25
     ink2  on chalk    6.78      chalk on rail    6.35
     ink3  on chalk    4.69      ink   on banner  8.28
     white on deep     9.50      chalk on miss    5.81

   =========================================================================== */

:root {
    /* ---- ground and surfaces ------------------------------------------- */
    --cloth: #3E8F4E;        /* the ground. carries no text, ever            */
    --cloth-deep: #35803F;   /* the same ground, one step back               */
    --deep: #1F4F2A;         /* recesses, trays, the action rail             */
    --deep-2: #173D20;       /* a pressed recess                             */
    --chalk: #F2EFE4;        /* every reading surface                        */
    --chalk-2: #E6E2D3;      /* a chalk surface one step back                */
    --rail: #7A4B2E;         /* the top bar - the table's wooden rail        */
    --rail-2: #5E3922;

    /* ---- ink ------------------------------------------------------------ */
    --ink: #1B1A15;          /* body and headings on chalk                   */
    --ink-2: #55524A;        /* secondary text on chalk                      */
    --ink-3: #6E6A5E;        /* the quietest thing that is still readable    */
    --hair: rgba(27, 26, 21, .14);
    --hair-2: rgba(27, 26, 21, .28);

    /* ---- signal --------------------------------------------------------- */
    /* Banner amber is the instruction banner printed on all 227 cards, so the
       app's loudest colour and the deck's loudest colour are the same one. */
    --banner: #F0A32B;
    --banner-2: #D98E1C;
    --miss: #A8321D;
    --miss-2: #8C2917;
    --made: #2E7D3A;         /* a made shot, on chalk                        */
    --mark: #4A6FA5;         /* the camera mark - cool, so it is never a score */

    /* ---- type ----------------------------------------------------------- */
    /* Sizes are real at 1180x820 - iPad landscape, which is what the table
       surface is measured against - and clamp so a phone and a desktop both
       stay sane. vmin, not vw: rotating the iPad must not resize the words. */
    --f-sys: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --t-hero: clamp(26px, 4.4vmin, 52px);
    --t-h1: clamp(20px, 3.0vmin, 34px);
    --t-h2: clamp(17px, 2.4vmin, 27px);
    --t-lead: clamp(15px, 2.0vmin, 22px);
    --t-body: clamp(13.5px, 1.8vmin, 19px);
    --t-small: clamp(12px, 1.55vmin, 16px);
    --t-micro: clamp(11px, 1.35vmin, 14px);
    --t-num: clamp(22px, 3.6vmin, 44px);   /* a score, read across the room  */

    /* ---- space ---------------------------------------------------------- */
    --s-1: 4px;
    --s-2: 8px;
    --s-3: 12px;
    --s-4: 16px;
    --s-5: 24px;
    --s-6: 32px;
    --s-7: 48px;

    /* ---- shape ---------------------------------------------------------- */
    /* The deck's own furniture is chamfered, not rounded. The app borrows the
       chamfer for anything that stands for a card, and keeps plain radii for
       controls, so the two never read as the same kind of object. */
    --r-1: 6px;
    --r-2: 10px;
    --r-3: 14px;
    --r-pill: 999px;
    --chamfer: 14px;
    --shadow-1: 0 1px 2px rgba(0, 0, 0, .10);
    --shadow-2: 0 6px 18px rgba(0, 0, 0, .16);

    /* ---- the action rail ------------------------------------------------ */
    /* 158px, fixed. It is the one part of the screen that must not move
       between a shot card, a drill level and an exam part: the hand learns
       where MADE is and should never have to look. */
    --rail-h: 158px;
    --rail-btn-h: 110px;      /* the primary targets inside it               */
    --tap-min: 44px;          /* nothing smaller is a touch target           */
    --top-h: 56px;            /* the nav bar, fixed so the surface can do maths */

    /* ---- the page under everything -------------------------------------- */
    /* A warm neutral, one step darker than chalk, so a chalk panel is visibly
       a panel. Pure white made every reading surface invisible against it. */
    --ground: #E4E0D0;
    --gutter: 16px;          /* the page's own side padding                   */

    /* ---- charts ---------------------------------------------------------- */
    /* One series per chart, so no chart needs a legend. Cool, because a
       measurement is not a verdict; the warm colours are reserved for things
       that happened at the table. --attention is the only chart colour that is
       ever set as text, which is why it is the dark red and not the amber:
       amber on chalk is 1.83:1 and unreadable. */
    --series: #3F6DA8;
    --series-soft: #CBD9EC;
    --attention: var(--miss);
}

/* The body's default 8px margin is what made the full-bleed bars overhang.
   A bar that cancels the page gutter cannot also be fighting a margin it
   cannot see: every page scrolled sideways by 8px until this was zeroed. */
html, body { background: var(--ground); margin: 0; }

/* The page gutter, owned here rather than by a Bootstrap utility class.
   .content used to carry `px-4`, whose padding is declared !important, so
   nothing here could reason about it - and the full-bleed bars below have to
   know it exactly to cancel it. */
.page.pp > main > .content { padding: 1.1rem var(--gutter) 0; }

/* ===========================================================================
   Page frame
   =========================================================================== */

.pp {
    font-family: var(--f-sys);
    color: var(--ink);
    font-size: var(--t-body);
    line-height: 1.45;
}

.pp h1 { font-size: var(--t-h1); margin: 0; font-weight: 700; letter-spacing: -.01em; }
.pp h2 { font-size: var(--t-h2); margin: 0; font-weight: 700; }
.pp p { margin: 0 0 var(--s-3); }
.pp p:last-child { margin-bottom: 0; }

/* A reading surface. Everything with words on it is one of these. */
.pp-panel {
    background: var(--chalk);
    border: 1px solid var(--hair);
    border-radius: var(--r-3);
    padding: var(--s-4) var(--s-5);
    box-shadow: var(--shadow-1);
}

/* A panel that stands for a card, so it wears the deck's chamfer. */
.pp-card-panel {
    background: var(--chalk);
    border: 1px solid var(--hair);
    padding: var(--s-4) var(--s-5);
    clip-path: polygon(
        var(--chamfer) 0, 100% 0, 100% calc(100% - var(--chamfer)),
        calc(100% - var(--chamfer)) 100%, 0 100%, 0 var(--chamfer));
}

.pp-muted { color: var(--ink-2); }
.pp-quiet { color: var(--ink-3); font-size: var(--t-small); }
.pp-spacer { flex: 1 1 auto; }
.pp-nums { font-variant-numeric: tabular-nums; }

/* ===========================================================================
   Controls
   =========================================================================== */

.pp-btn {
    font: inherit;
    font-size: var(--t-small);
    font-weight: 600;
    padding: 8px 16px;
    min-height: var(--tap-min);
    border-radius: var(--r-1);
    border: 1px solid var(--hair-2);
    background: var(--chalk);
    color: var(--ink);
    cursor: pointer;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    touch-action: manipulation;
    -webkit-user-select: none;
    user-select: none;
}

.pp-btn:hover { background: var(--chalk-2); }
.pp-btn:disabled { opacity: .38; cursor: default; }
.pp-btn:focus-visible { outline: 3px solid var(--banner); outline-offset: 2px; }

.pp-btn.primary {
    background: var(--banner);
    border-color: var(--banner-2);
    color: var(--ink);
}
.pp-btn.primary:hover { background: var(--banner-2); }

.pp-btn.ghost { background: transparent; border-color: var(--hair-2); }

/* The one big Start. It names what it opens rather than saying "Start". */
.pp-start {
    display: flex;
    align-items: center;
    gap: var(--s-4);
    width: 100%;
    text-align: left;
    font: inherit;
    padding: var(--s-4) var(--s-5);
    min-height: 96px;
    border: none;
    cursor: pointer;
    background: var(--banner);
    color: var(--ink);
    text-decoration: none;          /* it is a link, but it is a button */
    clip-path: polygon(
        var(--chamfer) 0, 100% 0, 100% calc(100% - var(--chamfer)),
        calc(100% - var(--chamfer)) 100%, 0 100%, 0 var(--chamfer));
    box-shadow: var(--shadow-2);
    touch-action: manipulation;
}
.pp-start, .pp-start:hover, .pp-start:focus { text-decoration: none; }
.pp-start * { text-decoration: none; }
.pp-start:hover { background: var(--banner-2); }
.pp-start .lede { font-size: var(--t-micro); text-transform: uppercase;
                  letter-spacing: .10em; opacity: .78; display: block; }
.pp-start .what { font-size: var(--t-h1); font-weight: 700; display: block; }
.pp-start .why { font-size: var(--t-small); opacity: .82; display: block; }
.pp-start .go { margin-left: auto; font-size: var(--t-h1); font-weight: 700; }

/* ===========================================================================
   Kind, carried by shape as well as colour
   ---------------------------------------------------------------------------
   Glare eats saturation before it eats geometry, so under the lamps the four
   kinds have to be told apart by outline. Shot is chamfered like a card, drill
   is a pill, exam is a square-cornered tab, knowledge is a full round.
   =========================================================================== */

.pp-kind {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--t-micro);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    padding: 4px 12px;
    background: var(--chalk-2);
    color: var(--ink-2);
    border: 1px solid var(--hair-2);
    white-space: nowrap;
}

.pp-kind.shot {
    border-radius: 0;
    clip-path: polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px);
}
.pp-kind.drill { border-radius: var(--r-pill); }
.pp-kind.exam { border-radius: 2px; }
.pp-kind.knowledge { border-radius: var(--r-pill); padding: 4px 14px; font-style: italic; }

/* ===========================================================================
   The day, as tappable chips
   =========================================================================== */

.pp-chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.pp-chip {
    font: inherit;
    font-size: var(--t-small);
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    padding: 9px 15px;
    min-height: var(--tap-min);
    background: var(--chalk);
    border: 1px solid var(--hair-2);
    color: var(--ink);
    cursor: pointer;
    text-decoration: none;
    touch-action: manipulation;
    clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
}
.pp-chip:hover { background: var(--chalk-2); }
.pp-chip .n { font-weight: 700; color: var(--ink-3); font-variant-numeric: tabular-nums; }

.pp-chip.drill { border-radius: var(--r-pill); clip-path: none; }
.pp-chip.exam { border-radius: 2px; clip-path: none; }

/* Due items are amber and appended - a guarantee that came due is the one
   thing on the day that was not chosen. */
.pp-chip.due { background: var(--banner); border-color: var(--banner-2); font-weight: 700; }
.pp-chip.due .n { color: var(--ink); opacity: .7; }

.pp-chip.done { background: var(--chalk-2); color: var(--ink-3); text-decoration: line-through; }
.pp-chip.now { box-shadow: 0 0 0 3px var(--banner); }

/* ===========================================================================
   The action rail
   ---------------------------------------------------------------------------
   Fixed height, same geometry for a shot card, a drill level and an exam part.
   Only the middle of the screen changes.
   =========================================================================== */

.pp-rail {
    flex: 0 0 auto;
    height: var(--rail-h);
    box-sizing: border-box;   /* 158px is the rail, not the rail plus its padding */
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    background: var(--deep);
    border-top: 3px solid var(--deep-2);
}

.pp-rail .slot {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    min-width: 0;
}

.pp-rail button {
    font: inherit;
    font-weight: 800;
    letter-spacing: .04em;
    border: none;
    border-radius: var(--r-2);
    cursor: pointer;
    touch-action: manipulation;
    -webkit-user-select: none;
    user-select: none;
    height: var(--rail-btn-h);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.pp-rail button:disabled { opacity: .32; cursor: default; }
.pp-rail button:focus-visible { outline: 3px solid var(--chalk); outline-offset: 3px; }

/* act-made and act-miss, not made and miss: a filled box in this same rail
   carries .made, and `.pp-rail .made` would have stretched every one of them
   to a 180px-wide, 110px-tall primary button. Same class, same container, two
   entirely different objects - so they do not share a name. */
.pp-rail .act-made {
    flex: 1 1 0; min-width: 180px;
    background: var(--banner); color: var(--ink);
    font-size: var(--t-h1);
}
.pp-rail .act-miss {
    flex: 1 1 0; min-width: 180px;
    background: var(--miss); color: var(--chalk);
    font-size: var(--t-h1);
}
.pp-rail .undo, .pp-rail .mark {
    width: 118px; background: var(--chalk); color: var(--ink);
    font-size: var(--t-small); flex-direction: column; gap: 2px;
}
.pp-rail .mark.on { background: var(--mark); color: #fff; }
.pp-rail .glyph { font-size: var(--t-h2); font-weight: 700; }

/* Where the session stands, read off the rail without looking away. */
.pp-rail .state {
    color: var(--chalk);
    font-size: var(--t-small);
    line-height: 1.3;
    padding: 0 var(--s-2);
    min-width: 120px;
}
.pp-rail .state b { font-size: var(--t-lead); display: block; font-variant-numeric: tabular-nums; }

/* ---- the scores, for a drill level ----------------------------------------
   A drill attempt is not made or missed - it is a number from 0 to MaxScore,
   up to sixteen of them - so this is the rail's middle on that surface. Each
   button carries what the score is worth: the count so far today, and the two
   probabilities (complete, advance) that used to occupy a whole screen of
   tiles. They are small because they are read after the number, not instead
   of it, and they are on a dark recess rather than on the cloth.
   --------------------------------------------------------------------------- */
.pp-scores {
    flex: 1 1 auto; min-width: 0;
    display: flex; flex-wrap: wrap; gap: 6px;
    align-content: center; justify-content: center;
}
.pp-rail .pp-score {
    flex: 0 0 auto;
    width: 72px; height: 68px;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 1px;
    border: 2px solid rgba(242, 239, 228, .30);
    border-radius: var(--r-1);
    background: rgba(242, 239, 228, .08);
    color: var(--chalk);
    font: inherit; cursor: pointer; touch-action: manipulation;
    -webkit-user-select: none; user-select: none;
}
.pp-score:hover { background: rgba(242, 239, 228, .20); }
.pp-score:focus-visible { outline: 3px solid var(--chalk); outline-offset: 2px; }
/* A score you have already put up today. Amber, the same signal the rest of
   the app uses for "this happened at the table". */
.pp-score.played { border-color: var(--banner); }
.pp-score { position: relative; }
.pp-score .n { font-size: var(--t-h2); font-weight: 800; line-height: 1;
               font-variant-numeric: tabular-nums; }
/* Times you have put this score up today. */
.pp-score .c {
    position: absolute; top: 3px; right: 4px;
    font-size: 10px; font-weight: 800; line-height: 1;
    padding: 2px 5px; border-radius: var(--r-pill);
    background: var(--banner); color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.pp-score .p { font-size: 10px; letter-spacing: .02em; opacity: .72;
               font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Sixteen scores will not fit beside UNDO and the readout on a narrow window.
   Two rows inside the same 158px is the right answer - the rail's height is
   what must not move, not the number of rows in it. */
@media (max-width: 1100px) {
    .pp-score { width: 62px; height: 62px; }
    .pp-score .p { display: none; }
}

/* ---- the exam runner's middle ---------------------------------------------
   Moving between parts, and the one action that cannot be undone. Finish is
   the amber primary; Reset is deliberately the quietest thing in the rail,
   because it throws the attempt away. */
.pp-exam-nav { flex: 1 1 auto; display: flex; gap: var(--s-3); align-items: center;
               justify-content: center; min-width: 0; }
.pp-exam-nav button { height: var(--rail-btn-h); border: none; border-radius: var(--r-2);
                      font: inherit; font-weight: 800; letter-spacing: .04em; cursor: pointer; }
.pp-exam-nav .next { flex: 1 1 0; min-width: 150px; max-width: 280px;
                     background: var(--chalk); color: var(--ink); font-size: var(--t-lead); }
.pp-exam-nav .finish { flex: 1 1 0; min-width: 180px; max-width: 320px;
                       background: var(--banner); color: var(--ink); font-size: var(--t-lead); }
.pp-exam-nav .reset { width: 110px; background: rgba(242, 239, 228, .10);
                      color: rgba(242, 239, 228, .82); font-size: var(--t-small); font-weight: 600;
                      border: 1px solid rgba(242, 239, 228, .28); }
.pp-exam-nav button:disabled { opacity: .32; cursor: default; }
/* The exam runner is not on a .pp-surface - its body is two columns that
   scroll - so its rail has to reach the edges itself. A rail that stops short
   of the screen is not in the same place as the other two. */
.exam-run .pp-rail { margin: var(--s-4) calc(-1 * var(--gutter)) 0; }

/* The boxes live in the rail too, so the score and the buttons are one object. */
/* Ten of these have to sit in the rail beside UNDO, the readout, MISSED,
   MADE and MARK. They wrap to two rows inside the same 158px rather than
   squeezing the buttons - the rail's height is what must not move. */
.pp-boxes { display: flex; gap: 5px; flex-wrap: wrap; align-items: center;
            justify-content: center; max-width: 280px; }   /* five per row, twice */
/* Scoped under .pp-rail, because `.pp-rail button` sets the 110px height of a
   primary target and beats a bare `.pp-box` on specificity. Unscoped, ten
   boxes wrapped to two rows of 110px and spilled 30px out of the top and the
   bottom of the rail. */
.pp-rail .pp-box {
    width: 52px; height: 52px;
    border: 2px solid rgba(242, 239, 228, .35);
    border-radius: var(--r-1);
    background: rgba(242, 239, 228, .10);
    color: rgba(242, 239, 228, .55);
    font: inherit; font-weight: 800; font-size: var(--t-lead); line-height: 1;
    display: flex; align-items: center; justify-content: center;
    padding: 0; cursor: pointer; touch-action: manipulation;
}
.pp-rail .pp-box.made { background: var(--banner); border-color: var(--banner-2); color: var(--ink); }
.pp-rail .pp-box.miss { background: var(--miss); border-color: var(--miss-2); color: var(--chalk); }
.pp-rail .pp-box.next { border-color: var(--banner); box-shadow: inset 0 0 0 2px var(--banner); color: var(--chalk); }
.pp-rail .pp-box.later { cursor: default; opacity: .45; }
/* A progression's boxes are disabled because the two buttons take its shots -
   but they are the record of the go, so they must not fade like a dead
   control. Only the cursor changes. */
.pp-rail .pp-box:disabled { cursor: default; opacity: 1; }
.pp-rail .pp-box.later:disabled { opacity: .45; }

/* ===========================================================================
   The table surface - the card, shown as large as the screen allows
   =========================================================================== */

/* ---------------------------------------------------------------------------
   THE ONE THING THIS GRID IS FOR.

   The old page was a flex column with `min-height`, and the card inside it
   carried `max-height: 100%`. A percentage needs a definite containing block
   to resolve against, and a flex column sized only by min-height does not
   have one - so the max-height was ignored, the card rendered at its natural
   900px, and on a 1366x1024 iPad in landscape the action rail was pushed off
   the bottom of the screen and the page scrolled.

   A grid row of `minmax(0, 1fr)` inside a grid of definite height IS definite,
   so percentages inside the stage resolve. That is the whole fix, and it is
   why the height here is `calc()` against a nav bar of fixed height rather
   than a min-height that adapts.
   --------------------------------------------------------------------------- */
.pp-surface {
    display: grid;
    grid-template-rows: auto auto minmax(0, 1fr) auto;   /* head, brief, stage, rail */
    height: calc(100dvh - var(--top-h));
    overflow: hidden;
    background: var(--cloth);
    margin: -1.1rem calc(-1 * var(--gutter)) 0;   /* out to the edges */
    padding: 0;
}
/* Older engines without dvh still get a working page, one URL-bar short. */
@supports not (height: 100dvh) {
    .pp-surface { height: calc(100vh - var(--top-h)); }
}

.pp-surface .head {
    flex: 0 0 auto;
    display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
    padding: var(--s-3) var(--s-4);
    background: var(--rail);
    color: var(--chalk);
    border-bottom: 3px solid var(--rail-2);
}
.pp-surface .head h1 { font-size: var(--t-h2); color: var(--chalk); }
/* The card's own number, subdued: smaller, and never the same colour as the
   name it belongs to. Brian asked for exactly this. */
.pp-surface .head .num {
    font-size: var(--t-small); font-weight: 700; color: rgba(242, 239, 228, .62);
    font-variant-numeric: tabular-nums;
}
.pp-surface .head .btn {
    font: inherit; font-size: var(--t-small); font-weight: 600;
    padding: 7px 13px; min-height: var(--tap-min);
    border-radius: var(--r-1); border: 1px solid rgba(242, 239, 228, .35);
    background: rgba(242, 239, 228, .10); color: var(--chalk); cursor: pointer;
    text-decoration: none; display: inline-flex; align-items: center;
}
.pp-surface .head .btn:hover { background: rgba(242, 239, 228, .20); }
.pp-surface .head .btn:disabled { opacity: .35; cursor: default; }

/* The instruction, in its own box, on chalk - never over the cloth. */
.pp-brief {
    flex: 0 0 auto;
    margin: var(--s-3) var(--s-4) 0;
    background: var(--chalk);
    color: var(--ink);
    font-size: var(--t-lead);
    line-height: 1.35;
    padding: var(--s-3) var(--s-4);
    border-left: 6px solid var(--banner);
    clip-path: polygon(0 0, calc(100% - var(--chamfer)) 0, 100% var(--chamfer),
                       100% 100%, 0 100%);
}
.pp-brief.tap { cursor: pointer; }
.pp-brief .more { color: var(--ink-3); font-size: var(--t-small); margin-left: var(--s-2); }

/* min-height on the surface means a percentage max-height on the image has no
   definite parent to resolve against, and the card renders at its natural
   900px - which is what pushed the rail off a 1366x1024 iPad. A flex child
   with min-height:0 and an explicit basis gives it one. */
.pp-stage {
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s-3) var(--s-4);
    overflow: hidden;
}
.pp-stage > img {
    max-height: 100%;
    max-width: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    display: block;
}

/* The table fills its row rather than sitting at whatever size the file
   happens to be. The crop is served at 1220x678, so on an iPad this is a
   scale of about 1.0 and never a visible upscale. */
.pp-stage > img.table { height: 100%; width: auto; max-width: 100%; }

/* ---- the table -------------------------------------------------------------
   The card is served already cropped to its wooden rail (see
   ShotCard.TableImage for why it is a file and not a CSS crop), so this is a
   plain replaced element with both maxima set - which letterboxes correctly in
   every engine and needs no aspect-ratio, no wrapper and no ghost.
   --------------------------------------------------------------------------- */

/* ===========================================================================
   Library - the deck by family
   =========================================================================== */

.pp-grid {
    display: grid;
    gap: var(--s-3);
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    align-content: start;
}
.pp-grid button {
    padding: 0; border: 2px solid transparent; background: var(--deep);
    cursor: pointer; overflow: hidden; display: block; border-radius: var(--r-2);
}
.pp-grid button:hover, .pp-grid button:focus-visible { border-color: var(--banner); outline: none; }
/* Every card is 1200x900, declared up front: 165 lazy images with no intrinsic
   size all resolve to the height of their border at first layout and never
   grow once the files arrive. That happened in production. */
.pp-grid img { width: 100%; aspect-ratio: 4 / 3; height: auto; display: block;
               object-fit: cover; background: var(--deep); }
.pp-grid .cap {
    display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-2);
    padding: 6px 10px 8px; color: var(--chalk); font-size: var(--t-micro);
    font-weight: 700; text-align: left;
}
.pp-grid .cap em { font-style: normal; font-weight: 400; opacity: .72; }

/* ===========================================================================
   Top bar
   =========================================================================== */

/* Fixed height, and it never wraps. Everything below is sized against
   100dvh minus this bar, so a bar that changed height with the window would
   make the action rail drift off the bottom of the screen - which is exactly
   the class of bug the surface below is built to avoid. It scrolls sideways
   on a narrow phone instead of growing. */
.pp-top {
    display: flex; align-items: center; gap: 2px;
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    height: var(--top-h); box-sizing: border-box;
    background: var(--rail);
    padding: 0 var(--gutter);
    border-bottom: 3px solid var(--rail-2);
}
.pp-top::-webkit-scrollbar { display: none; }
.pp-top a { flex: 0 0 auto; white-space: nowrap; }
.pp-top a {
    color: var(--chalk); text-decoration: none; font-size: var(--t-small);
    font-weight: 600; padding: 10px 14px; border-radius: var(--r-1);
    min-height: var(--tap-min); display: inline-flex; align-items: center;
}
.pp-top a:hover { background: rgba(242, 239, 228, .14); }
.pp-top a.on { background: var(--chalk); color: var(--ink); }
.pp-top .pp-spacer { flex: 1 1 auto; }

@media print {
    .pp-rail, .pp-top { display: none; }
}
