/*
    FILE:   html/css/portraits.css
    AUTHOR: David Shaw
    DATE:   2026-08-22
    DESC:   Character-portrait sprite sheet (www/images/RC_Char_Spritesheet.jpg,
            7 columns x 12 rows, uniform 128x150 cells). One element crops a
            single cell via background-position. Indices 1-34 are
            RC_Char_{sprintf('%02d', N)}.jpg from the original ASP
            character-creation page; indices 35-83 are new art appended
            2026-08-22 (7 more rows, no legacy RC_Char_NN.jpg equivalent -
            see db/thestarsbeyond-character-gfx-spritesheet-manifest.json,
            which only carries entries for the legacy 1-34). Grid geometry
            is otherwise shared by app/Helpers/PortraitHelper.php. Index 0 is
            RC_Frame.jpg, the ASP picker's own "nothing selected"
            placeholder - used directly by character/lobby.php's New
            Character card (data-portrait="0", never variable), but NOT a
            valid PORTRAITINDEX value for an actual character:
            PortraitHelper::isValid() requires 1-83, and the create.php
            picker's carousel starts at 1, because PORTRAITINDEX NULL
            already means "not chosen" for a real character. Two different
            things: NULL is "this character has no portrait yet", index 0 is
            "here is a picture of no portrait".

            Replaces the 64-portrait DS_Portraits_All.jpg sheet (16 cols x 4
            rows), which used PERCENTAGE background-position/background-size
            - percentages here are relative to the whole sheet's dimensions,
            so appending a row shifted the position of every existing row's
            crop window, not just the new one (that's what caused the
            "creeping" bug documented in this file's git history: rows 2-3
            bled the row above into frame). That sheet's rows were also
            uneven height (152/174/151/124px), forcing a "crop to the
            shortest row" workaround on top of the percentage math.

            HOW TO ADD MORE PORTRAITS (permanent note - read this first):
            Cells here are uniform, and positioning is done in FIXED PIXELS
            via CSS custom properties, not percentages, so this does NOT
            recur. Growth is strictly append-only: the sheet is full through
            index 83 (row 11, col 6, using 0-based row/col here even though
            the portrait INDEX is 1-based) - keep the 7-column width fixed
            and grow downward, filling left-to-right within each new row
            (index 84 -> row 12 col 0, 85 -> row 12 col 1, ... wrapping to a
            new row every 7).
              1. Append the new 128x150 image(s) to the bottom of
                 RC_Char_Spritesheet.jpg, in that left-to-right, top-to-
                 bottom order.
              2. Add one new rule per index below, e.g. for index 84 at row
                 12 col 0: .portrait-sprite[data-portrait="84"] { --row: 12; --col: 0; }
              3. Bump the "1800px" in .portrait-sprite's background-size line
                 (below) to the sheet's new total height (rows x 150).
                 That's it - existing rules and existing indices are NEVER
                 touched. Also update PortraitHelper::TOTAL and the manifest
                 JSON's entries so the DB/PHP side agrees with the image.

            Position is set with a data-portrait="N" attribute + a static
            attribute-selector rule per index, NOT inline
            style="background-position: ...". This app's CSP sends both a
            nonce and 'unsafe-inline' for style-src (Core/View.php), but per
            spec a nonce present makes browsers ignore 'unsafe-inline'
            entirely, so any inline style="" silently never applies -
            responsive.css:1241 has the postmortem on this exact trap. Same
            reason planets.css uses .sun-disc[data-sun="red"] instead of an
            inline custom property.

            Linked globally from common_head_tags.php (not per-page) so any
            future view that shows a portrait gets this for free.

            The picker (#portrait_grid, views/character/create.php) is a
            single-row horizontally-scrolling carousel, not the site's usual
            float-based col-N grid (responsive.css) - see the #portrait_grid
            rule below. character/lobby.php's single-portrait cards are
            unaffected; that's still one .portrait-sprite--card each, no
            grid or carousel involved.
*/

.portrait-sprite {
    display: inline-block;
    /* figure's UA-stylesheet default is margin: 1em 40px - both
       portrait-sprite--thumb (in the #portrait_grid picker, spacing
       handled below) and portrait-sprite--card (character/lobby.php,
       which wants none - it sits inside .character-card__body's own
       flex gap, not a grid of adjacent portraits) need this reset, so
       it belongs on the shared base class rather than either modifier. */
    margin: 0;
    background-color: #1a1a1a;
    background-image: url('/images/RC_Char_Spritesheet.jpg?v=f77e719d');
    background-repeat: no-repeat;
    --scale: 1;
    width: calc(128px * var(--scale));
    height: calc(150px * var(--scale));
    /* Fixed pixels, not percentages - see the header note. 896px = 7 cols x
       128px (constant, columns never grow). 1800px = 12 rows x 150px so far -
       this is the ONE value to bump when a row is appended. */
    background-size: calc(896px * var(--scale)) calc(1800px * var(--scale));
    /* var(--row, 0)/var(--col, 0) fall back to row 0 col 0 - RC_Frame.jpg -
       for any data-portrait value with no matching rule below, same as
       omitting data-portrait entirely. That fallback landing on the frame
       is deliberate, not incidental: it's the one cell that's a sensible
       "nothing rendered yet" default rather than a real portrait. */
    background-position: calc(var(--col, 0) * -128px * var(--scale)) calc(var(--row, 0) * -150px * var(--scale));
}

/* A PLAYER'S UPLOADED PICTURE (dossier 26) - an <img> drawn by
   PortraitHelper::render() where the sheet cell would be. It wears the SAME
   size modifier as the cell (portrait-sprite--card, --scan, ...), and those
   set only --scale, so both branches are one size by construction. It
   ASSIGNS no --scale of its own - var(--scale, 1) - so a modifier wins
   whatever order the rules sit in (the ship sheet's lesson). The stored
   file is already cropped to 128:150; object-fit only covers rounding. */
.portrait-img {
    display: inline-block;
    margin: 0;
    width: calc(128px * var(--scale, 1));
    height: calc(150px * var(--scale, 1));
    object-fit: cover;
    object-position: top;
    background-color: #1a1a1a;
}

.portrait-sprite--thumb { --scale: 0.46875; } /* 60 / 128 */
.portrait-sprite--card  { --scale: 0.9375; }  /* 120 / 128 */
/* The governor portrait in the Star page's planet modal (views/star/
   show.php). 100px to match .planet--modal's own --size exactly, so the
   two stack as one column of equal width; the sheet's 128x150 cells
   make that 117px tall, which is the aspect ratio, not a stray value. */
.portrait-sprite--planet-detail { --scale: 0.78125; } /* 100 / 128 */
/* The owner portrait in the Star page's ship-scan modal (views/star/
   scan.php). Same 100px as --planet-detail above, and deliberately so:
   the two modals open into the same container and a portrait that changed
   size between them would read as a different kind of thing. Kept as its
   own modifier rather than reusing --planet-detail, because a ship scan
   borrowing a class named for planets is the sort of thing that survives
   until someone changes the planet modal and silently moves this too. */
.portrait-sprite--scan { --scale: 0.78125; } /* 100 / 128 */
/* The captain menu's toggle in the navbar (partials/nav/action_menu.php,
   author ruling 2026-09-18). 32px wide, filling the 36px button inside its
   2px ring; 37.5px tall, so the button's overflow:hidden and its circle
   crop the cell top and bottom rather than squashing the face. Here in
   portraits.css, beside its siblings, because common_head_tags.php emits
   responsive.css FIRST - a one-class rule there would lose to
   .portrait-sprite's own --scale: 1 on source order. */
.portrait-sprite--menu { --scale: 0.25; flex-shrink: 0; } /* 32 / 128 */
/* The explorer's portrait on the planet page's survey plaque (views/planet/
   show.php) - 1999 drew it at 62 x 72, and 62 / 128 keeps that. */
.portrait-sprite--plaque { --scale: 0.484375; flex: 0 0 auto; } /* 62 / 128 */
/* A player captain on the combat report (views/combat/report.php): the
   report's pictures are 5rem tall, so 80 / 150. A block, or the line box's
   descender space floats the name capsule clear of the picture. */
.portrait-sprite--combat { --scale: 0.533333; display: block; } /* 80 / 150 */

.portrait-sprite--thumb.is-selected {
    outline: 3px solid #485fc7;
    outline-offset: -3px;
}

/* Picker carousel (views/character/create.php): a single row, never wraps,
   scrolls horizontally for the rest. Each thumbnail box is 60+16=76px
   (thumb width + 8px margin each side, same spacing math as before - a
   boundary edge is padding+margin, a shared edge between two thumbnails is
   margin+margin, both land on 16px). max-width below is sized to 8.5
   boxes (8 full, one peeking at the edge) so it's visually obvious there's
   more to scroll to, without needing JS to detect overflow. scroll-snap
   makes wheel/trackpad/touch scrolling settle on a thumbnail boundary
   instead of stopping mid-item. */
#portrait_grid {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    max-width: calc(8.5 * (60px + 16px));
    padding: 8px;
}

#portrait_grid .portrait-sprite--thumb {
    flex: 0 0 auto;
    scroll-snap-align: start;
    margin: 8px;
}

.portrait-sprite[data-portrait="1"]  { --row: 0; --col: 1; }
.portrait-sprite[data-portrait="2"]  { --row: 0; --col: 2; }
.portrait-sprite[data-portrait="3"]  { --row: 0; --col: 3; }
.portrait-sprite[data-portrait="4"]  { --row: 0; --col: 4; }
.portrait-sprite[data-portrait="5"]  { --row: 0; --col: 5; }
.portrait-sprite[data-portrait="6"]  { --row: 0; --col: 6; }
.portrait-sprite[data-portrait="7"]  { --row: 1; --col: 0; }
.portrait-sprite[data-portrait="8"]  { --row: 1; --col: 1; }
.portrait-sprite[data-portrait="9"]  { --row: 1; --col: 2; }
.portrait-sprite[data-portrait="10"] { --row: 1; --col: 3; }
.portrait-sprite[data-portrait="11"] { --row: 1; --col: 4; }
.portrait-sprite[data-portrait="12"] { --row: 1; --col: 5; }
.portrait-sprite[data-portrait="13"] { --row: 1; --col: 6; }
.portrait-sprite[data-portrait="14"] { --row: 2; --col: 0; }
.portrait-sprite[data-portrait="15"] { --row: 2; --col: 1; }
.portrait-sprite[data-portrait="16"] { --row: 2; --col: 2; }
.portrait-sprite[data-portrait="17"] { --row: 2; --col: 3; }
.portrait-sprite[data-portrait="18"] { --row: 2; --col: 4; }
.portrait-sprite[data-portrait="19"] { --row: 2; --col: 5; }
.portrait-sprite[data-portrait="20"] { --row: 2; --col: 6; }
.portrait-sprite[data-portrait="21"] { --row: 3; --col: 0; }
.portrait-sprite[data-portrait="22"] { --row: 3; --col: 1; }
.portrait-sprite[data-portrait="23"] { --row: 3; --col: 2; }
.portrait-sprite[data-portrait="24"] { --row: 3; --col: 3; }
.portrait-sprite[data-portrait="25"] { --row: 3; --col: 4; }
.portrait-sprite[data-portrait="26"] { --row: 3; --col: 5; }
.portrait-sprite[data-portrait="27"] { --row: 3; --col: 6; }
.portrait-sprite[data-portrait="28"] { --row: 4; --col: 0; }
.portrait-sprite[data-portrait="29"] { --row: 4; --col: 1; }
.portrait-sprite[data-portrait="30"] { --row: 4; --col: 2; }
.portrait-sprite[data-portrait="31"] { --row: 4; --col: 3; }
.portrait-sprite[data-portrait="32"] { --row: 4; --col: 4; }
.portrait-sprite[data-portrait="33"] { --row: 4; --col: 5; }
.portrait-sprite[data-portrait="34"] { --row: 4; --col: 6; }

/* 35-83: new art appended 2026-08-22 (7 more rows) - no legacy RC_Char_NN.jpg
   equivalent, see this file's header. */
.portrait-sprite[data-portrait="35"] { --row: 5; --col: 0; }
.portrait-sprite[data-portrait="36"] { --row: 5; --col: 1; }
.portrait-sprite[data-portrait="37"] { --row: 5; --col: 2; }
.portrait-sprite[data-portrait="38"] { --row: 5; --col: 3; }
.portrait-sprite[data-portrait="39"] { --row: 5; --col: 4; }
.portrait-sprite[data-portrait="40"] { --row: 5; --col: 5; }
.portrait-sprite[data-portrait="41"] { --row: 5; --col: 6; }
.portrait-sprite[data-portrait="42"] { --row: 6; --col: 0; }
.portrait-sprite[data-portrait="43"] { --row: 6; --col: 1; }
.portrait-sprite[data-portrait="44"] { --row: 6; --col: 2; }
.portrait-sprite[data-portrait="45"] { --row: 6; --col: 3; }
.portrait-sprite[data-portrait="46"] { --row: 6; --col: 4; }
.portrait-sprite[data-portrait="47"] { --row: 6; --col: 5; }
.portrait-sprite[data-portrait="48"] { --row: 6; --col: 6; }
.portrait-sprite[data-portrait="49"] { --row: 7; --col: 0; }
.portrait-sprite[data-portrait="50"] { --row: 7; --col: 1; }
.portrait-sprite[data-portrait="51"] { --row: 7; --col: 2; }
.portrait-sprite[data-portrait="52"] { --row: 7; --col: 3; }
.portrait-sprite[data-portrait="53"] { --row: 7; --col: 4; }
.portrait-sprite[data-portrait="54"] { --row: 7; --col: 5; }
.portrait-sprite[data-portrait="55"] { --row: 7; --col: 6; }
.portrait-sprite[data-portrait="56"] { --row: 8; --col: 0; }
.portrait-sprite[data-portrait="57"] { --row: 8; --col: 1; }
.portrait-sprite[data-portrait="58"] { --row: 8; --col: 2; }
.portrait-sprite[data-portrait="59"] { --row: 8; --col: 3; }
.portrait-sprite[data-portrait="60"] { --row: 8; --col: 4; }
.portrait-sprite[data-portrait="61"] { --row: 8; --col: 5; }
.portrait-sprite[data-portrait="62"] { --row: 8; --col: 6; }
.portrait-sprite[data-portrait="63"] { --row: 9; --col: 0; }
.portrait-sprite[data-portrait="64"] { --row: 9; --col: 1; }
.portrait-sprite[data-portrait="65"] { --row: 9; --col: 2; }
.portrait-sprite[data-portrait="66"] { --row: 9; --col: 3; }
.portrait-sprite[data-portrait="67"] { --row: 9; --col: 4; }
.portrait-sprite[data-portrait="68"] { --row: 9; --col: 5; }
.portrait-sprite[data-portrait="69"] { --row: 9; --col: 6; }
.portrait-sprite[data-portrait="70"] { --row: 10; --col: 0; }
.portrait-sprite[data-portrait="71"] { --row: 10; --col: 1; }
.portrait-sprite[data-portrait="72"] { --row: 10; --col: 2; }
.portrait-sprite[data-portrait="73"] { --row: 10; --col: 3; }
.portrait-sprite[data-portrait="74"] { --row: 10; --col: 4; }
.portrait-sprite[data-portrait="75"] { --row: 10; --col: 5; }
.portrait-sprite[data-portrait="76"] { --row: 10; --col: 6; }
.portrait-sprite[data-portrait="77"] { --row: 11; --col: 0; }
.portrait-sprite[data-portrait="78"] { --row: 11; --col: 1; }
.portrait-sprite[data-portrait="79"] { --row: 11; --col: 2; }
.portrait-sprite[data-portrait="80"] { --row: 11; --col: 3; }
.portrait-sprite[data-portrait="81"] { --row: 11; --col: 4; }
.portrait-sprite[data-portrait="82"] { --row: 11; --col: 5; }
.portrait-sprite[data-portrait="83"] { --row: 11; --col: 6; }
