/*
    FILE:   html/css/planets.css
    AUTHOR: David Shaw
    DATE:   2026-08-20
    DESC:   The rotating-planet technique from docs/css-planets.html,
            adapted for views/star/show.php's orbit display. Three layers
            inside one circular element: ::before scrolls the surface
            texture (the rotation), .clouds optionally scrolls weather on
            its own period, ::after is a fixed shading/terminator gradient
            that sells the sphere. See docs/css-planets.html's own header
            comment for the mechanics (200%-wide texture, repeat-x,
            translate -50% per cycle = one seamless loop).

            --size is set via one of the 4 modifier classes below, not
            inline style="--size:..." - this app's CSP has no 'unsafe-
            inline' fallback for style-src once a nonce is present (see
            responsive.css's own fieldset.help comment for the history),
            so inline custom-property assignment would be silently
            dropped. Same 15/25/35/50 bucket the plain <img> planets in
            views/star/show.php already use (sp_Star_System_Display_New's
            own Size bucketing), so a class switching to this technique
            keeps the exact same on-screen size it had before.

            EVERY planets.GFX value migrated to a real DS_*.jpg photo
            (db/thestarsbeyond-planet-gfx-migration.sql) is wired here,
            keyed by that exact filename rather than by planets.class -
            Gas Giant alone has 7 distinct textures, so the map has to be
            per-variant or six of them would render identically. All are
            already 400x200 (verified against every file this migration
            touched), exactly what this technique expects, so no resizing
            was needed. No --clouds/--caps on any of them: these are real
            photos with weather/ice already baked in, same reasoning as
            the original terra entry. Only Asteroid Belt/Depot/Outpost/
            Station keep their old RC_/DS_ static <img> - those are orbit
            features, not planets, and were never in scope for this.
*/

.planet {
    position: relative;
    width: var(--size);
    height: var(--size);
    display: block;
    margin: 0 auto;
    border-radius: 50%;
    overflow: hidden;
    isolation: isolate;
    flex: 0 0 auto;
    box-shadow: 0 0 calc(var(--size) * .16) color-mix(in srgb, var(--atmo) 45%, transparent);

    /* DEFAULTS, AND THEY ARE A REAL GUARD - AUTHOR RULING 2026-08-29.
       There was no --size fallback here at all, which was survivable while
       sp_Star_System_Display_New bucketed every planet into one of exactly
       four classes. It now returns the RAW planets.SIZE, so the class name
       comes straight from the data: a ninth value appearing in that column
       would match no rule below, leaving `width: var(--size)` invalid at
       computed-value time and the planet rendering at auto width. These two
       lines make that a 50px/24s planet instead of a broken one.
       QA/check_planet_sizes.php is what actually catches it. */
    --size: 50px;
    --period: 24s;
}

/* ROTATION IS DRIVEN BY planets.SIZE, AND LARGER BODIES SPIN FASTER -
   AUTHOR RULING 2026-08-29. This inverts what was here before, which
   scaled a per-body-type --period-base by --size / 50px so that a bigger
   planet turned SLOWER. Those 17 --period-base declarations are deleted;
   they lived only in this file and nothing else ever read them.

   The model is one second per real hour of rotation, anchored on Earth:

       T = 24s * (10 / SIZE) ^ 0.5758

   with the exponent solved so raw SIZE 50 lands at 9.5s - Jupiter's real
   9.9h. calc() cannot raise to a fractional power, so the formula is
   evaluated per value and the results are literals; the line above is what
   makes them re-derivable.

   IT FITS THE REAL SOLAR SYSTEM BETTER THAN IT HAS ANY RIGHT TO, once
   Saturn and Uranus were corrected: Neptune (raw 20) lands on 16.1s
   against a real 16.1h, Uranus 16.1 against 17.2, Mars 24 against 24.6.

   --size REPRODUCES THE CASE THE PROCEDURE USED TO APPLY, and this is now
   the only place that mapping is written:
       SIZE < 10 -> 30px    SIZE 25..49 -> 70px
       SIZE 10..24 -> 50px  SIZE >= 50  -> 100px
   Four rendered sizes, eight periods - which is the whole point of keying
   the classes on the raw value.

   SIZE 0 TAKES SIZE 5's VALUES (author ruling): the formula divides by
   SIZE and 0 has no answer. 5,349 planets carry it, and while nearly all
   are asteroid belts drawn as a plain <img>, exactly one renders as a
   rotating sphere - so it does need a period, not a hole.

   ORDER MATTERS: these must stay ABOVE .planet--modal, which shares their
   specificity and wins its --size only by coming later. */
.planet--size0  { --size: 30px;  --period: 35.8s; }
.planet--size5  { --size: 30px;  --period: 35.8s; }
.planet--size9  { --size: 30px;  --period: 25.5s; }
.planet--size10 { --size: 50px;  --period: 24s;   }
.planet--size15 { --size: 50px;  --period: 19s;   }
.planet--size20 { --size: 50px;  --period: 16.1s; }
.planet--size25 { --size: 70px;  --period: 14.2s; }
.planet--size50 { --size: 100px; --period: 9.5s;  }

/* Used only where the orbit grid's .planet div is cloned into the planet-
   detail modal (views/star/show.php's renderPlanetModal) - a fixed 100px
   whatever the orbit's own size, so every planet reads the same size once
   you are looking at it up close.

   IT OVERRIDES --size ONLY, AND THAT IS DELIBERATE - author ruling
   2026-08-29: the modal inherits the behaviour of its origin. The cloned
   div now carries its planet's own .planet--size{N} class as well, so
   --period comes from the real body while --size is forced to 100px here.
   A small fast planet therefore stays fast when opened, instead of every
   modal spinning at one speed. This rule sits after the size classes so
   its --size wins; it deliberately sets no --period, so theirs survives. */
.planet--modal { --size: 100px; }

.planet::before,
.planet .clouds {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 200%;
    height: 100%;
    background-size: 50% 100%;   /* one full map = one planet width */
    background-repeat: repeat-x; /* ...painted twice across the 200% box */
    animation: planet-meridian var(--period, 28s) linear infinite;
    will-change: transform;
}

.planet::before {
    background-image: var(--caps, none), var(--surface);
}

.planet .clouds {
    background-image: var(--clouds, none);
    opacity: var(--cloud-opacity, .5);
    animation-duration: var(--cloud-period, 20s);
}

/* Terminator + limb darkening + a rim of scattered light on the night
   edge. Light source sits upper-left at 30%/28%. Fixed - this is what
   makes a flat scrolling texture read as a rotating sphere.

   The linear-gradient layer (added 2026-08-20) is the day/night
   terminator itself, as a vertical band rather than the radial glow's
   circular falloff - a rotating sphere lit from one side shows a roughly
   straight terminator line, not a spotlight. Stacked as a middle layer:
   the radial highlight (specular point) stays on top of it, limb
   darkening stays underneath it, same left-lit direction as both (30%
   area lit, fading to shadow toward the right). */
.planet::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background:
        radial-gradient(circle at 30% 26%,
            rgba(255, 255, 255, .28) 0%,
            rgba(255, 255, 255, 0) 45%),
        linear-gradient(90deg,
            rgba(3, 6, 14, 0) 0%,
            rgba(3, 6, 14, 0) 40%,
            rgba(3, 6, 14, .55) 58%,
            rgba(3, 6, 14, .85) 100%),
        radial-gradient(circle at 33% 30%,
            rgba(3, 6, 14, 0) 38%,
            rgba(3, 6, 14, .55) 74%,
            rgba(3, 6, 14, .92) 100%);
    box-shadow:
        inset calc(var(--size) * -.015) calc(var(--size) * -.02)
        calc(var(--size) * .05)
        color-mix(in srgb, var(--atmo) 50%, transparent);
}

@keyframes planet-meridian {
    to { transform: translateX(-50%); }
}

/* Accessibility floor: hold a single frame, don't just pause mid-blur. */
@media (prefers-reduced-motion: reduce) {
    .planet::before,
    .planet .clouds {
        animation: none !important;
    }
}

/* Terran - the 3 variants sp_Star_System_Display_New actually assigns
   (Terran_3 is the dominant one at 8,674 rows; 1/2 are rare). */
.planet[data-body="terran-1"] {
    --atmo: #5aa9f0;
    --surface: url("/images/DS_Terran_1.jpg?v=c1a58ad6");
}
.planet[data-body="terran-2"] {
    --atmo: #5aa9f0;
    --surface: url("/images/DS_Terran_2.jpg?v=a55fcb92");
}
.planet[data-body="terran-3"] {
    --atmo: #5aa9f0;
    --surface: url("/images/DS_Terran_3.jpg?v=597c688c");
}

/* Barren - 3 variants, grey/brown rock, no atmosphere glow to speak of. */
.planet[data-body="barren-1"] {
    --atmo: #6b7385;
    --surface: url("/images/DS_Barren1a.jpg?v=32c8aa3f");
}
.planet[data-body="barren-2"] {
    --atmo: #6b7385;
    --surface: url("/images/DS_Barren2a.jpg?v=827ab3a6");
}
.planet[data-body="barren-3"] {
    --atmo: #6b7385;
    --surface: url("/images/DS_Barren3a.jpg?v=904fc3cf");
}

/* Frozen - 2 variants, pale ice-blue glow, slow rotation. */
.planet[data-body="frozen-1"] {
    --atmo: #bfe3f0;
    --surface: url("/images/DS_Frozen1a.jpg?v=6dead908");
}
.planet[data-body="frozen-2"] {
    --atmo: #bfe3f0;
    --surface: url("/images/DS_Frozen2a.jpg?v=779fa98a");
}

/* Gas Giant - 7 variants, same warm banded-cloud tone and rotation speed
   as each other; only the texture differs. */
.planet[data-body="gas-1"] {
    --atmo: #f0b968;
    --surface: url("/images/DS_Gas1a.jpg?v=0147b12d");
}
.planet[data-body="gas-2"] {
    --atmo: #f0b968;
    --surface: url("/images/DS_Gas2a.jpg?v=2d078e47");
}
.planet[data-body="gas-3"] {
    --atmo: #f0b968;
    --surface: url("/images/DS_Gas3a.jpg?v=df083856");
}
.planet[data-body="gas-4"] {
    --atmo: #f0b968;
    --surface: url("/images/DS_Gas4a.jpg?v=e381b082");
}
.planet[data-body="gas-5"] {
    --atmo: #f0b968;
    --surface: url("/images/DS_Gas5a.jpg?v=67c3e97b");
}
.planet[data-body="gas-6"] {
    --atmo: #f0b968;
    --surface: url("/images/DS_Gas6a.jpg?v=e4a7e306");
}
.planet[data-body="gas-7"] {
    --atmo: #f0b968;
    --surface: url("/images/DS_Gas7a.jpg?v=f05e6be9");
}

/* Molten - fiery orange-red glow, moderate rotation. */
.planet[data-body="molten"] {
    --atmo: #e8623c;
    --surface: url("/images/DS_Molten1a.jpg?v=893d05d4");
}

/* Hostile - sickly toxic-purple glow. */
.planet[data-body="hostile"] {
    --atmo: #8a3d6c;
    --surface: url("/images/DS_Hostile1a.jpg?v=82bbdce4");
}

/* ============================================================
   THE SUN - same scrolling-texture rotation as .planet above, minus
   ::after. A star is self-luminous - there's no day/night line or limb
   darkening to draw across its own light source, so the shading layer
   that sells a PLANET as a lit sphere would be wrong here, not just
   unnecessary. Kept: the circular clip, the box-shadow glow (an outward
   corona reads fine on a sun), the ::before scroll. Dropped: ::after
   entirely, and the automatic size-scaled --period (.planet's own
   --period-base * --size formula) - suns don't need that nuance yet, so
   --period is a flat per-colour value instead; add the same calc here
   later if that turns out wrong.

   AUTHOR RULING 2026-08-20: half the disc is deliberately hidden - see
   .star-orbit-cell--sun .star-orbit-cell__image-zone in responsive.css,
   which clips it. transform:translateX(-50%) here shifts the whole disc
   left by half its own width, flush first against the zone's own left
   edge (justify-content:flex-start on the zone, not the centring every
   other image-zone uses) so the shift and the zone's overflow:hidden land
   on exactly the disc's own halfway point regardless of how much wider
   the zone is than the disc - it does NOT shrink-wrap to the disc's size
   here (it inherits the same min-width:7rem every orbit column has).
   Purely a visual choice, not tied to any game data.

   box-shadow (not ::after) is doing the corona: a star is one light
   source with no visible far side, so a soft even bloom reads right in
   a way an off-centre highlight/terminator (.planet's ::after) wouldn't.
   It's the right tool for a second reason too - box-shadow paints outside
   the element's own box without being subject to that box's own
   overflow:hidden (only child CONTENT is), so it survives both clips in
   play here: the disc's own (around ::before's rotating texture) and the
   zone's (the half-hide above). A glow built as ::after instead would get
   clipped by the disc's own overflow:hidden the same way the texture is,
   and never show at all. Three stacked layers - tight bright core, a
   softer mid falloff, a wide faint outer bloom - read as a corona rather
   than a flat single-radius glow. */
.sun-disc {
    position: relative;
    width: var(--size);
    height: var(--size);
    display: block;
    border-radius: 50%;
    overflow: hidden;
    isolation: isolate;
    flex: 0 0 auto;
    box-shadow:
        0 0 calc(var(--size) * .08) calc(var(--size) * .02) color-mix(in srgb, var(--atmo, #fff) 75%, transparent),
        0 0 calc(var(--size) * .28) calc(var(--size) * .05) color-mix(in srgb, var(--atmo, #fff) 40%, transparent),
        0 0 calc(var(--size) * .6)  calc(var(--size) * .08) color-mix(in srgb, var(--atmo, #fff) 18%, transparent);
    transform: translateX(-50%);
    /* No margin:0 auto (unlike .planet, which needs it for the non-flex
       modal usage) - auto margins on a flex item consume free space
       ahead of justify-content, so left+right:auto here would silently
       re-centre the disc and cancel out the sun cell's own
       justify-content:flex-start (responsive.css) that the half-hidden
       effect depends on. Measured live: with margin:0 auto still in
       place, flex-start had no visible effect at all. */
}

.sun-disc--sun-small  { --size: 60px; }
.sun-disc--sun-medium { --size: 120px; }
.sun-disc--sun-large  { --size: 150px; }

.sun-disc::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 200%;
    height: 100%;
    background-size: 50% 100%;
    background-repeat: repeat-x;
    background-image: var(--surface);
    animation: planet-meridian var(--period, 40s) linear infinite;
    will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
    .sun-disc::before {
        animation: none !important;
    }
}

.sun-disc[data-sun="red"]    { --atmo: #ff5a3c; --period: 46s; --surface: url("/images/DS_SunRed.jpg?v=cf555dd8"); }
.sun-disc[data-sun="orange"] { --atmo: #ff9a3c; --period: 42s; --surface: url("/images/DS_SunOrange.jpg?v=f150ebbb"); }
.sun-disc[data-sun="yellow"] { --atmo: #ffd93c; --period: 38s; --surface: url("/images/DS_SunYellow.jpg?v=bb642ded"); }
.sun-disc[data-sun="white"]  { --atmo: #eef3ff; --period: 30s; --surface: url("/images/DS_SunWhite.jpg?v=c895a0e9"); }
.sun-disc[data-sun="blue"]   { --atmo: #6ab8ff; --period: 22s; --surface: url("/images/DS_SunBlue.jpg?v=8d452266"); }
.sun-disc[data-sun="brown"]  { --atmo: #a8763c; --period: 60s; --surface: url("/images/DS_SunBrown.jpg?v=3466c518"); }

/* ============================================================
   Wormhole - the Exit System cell's mouth
   ============================================================
   Replaces 1999's DS_wormhole.jpg, which sp_Star_System_Display_New emitted
   inside an HTML string. 50x50 is that image's own size, and the size the
   proc drew it at (floor(50 * dimMod), dimMod 1). The png is 208x208 and
   square, so it downscales without distortion.

   THE SPIN IS THE WHOLE POINT OF THE NEW FILE: it has a transparent
   background, so rotating it turns the disc without dragging a visible
   square corner around with it - which is exactly what the old jpg would
   have done.

   BOTH width AND height ARE SET. An <img> with only one dimension keeps its
   intrinsic aspect, which is right here, but the cell's layout is steadier
   if the box is fixed before the image loads. */
.wormhole-disc {
    width: 50px;
    height: 50px;
    display: block;
    margin: 0 auto;
    animation: wormhole-spin 18s linear infinite;
}

@keyframes wormhole-spin {
    to { transform: rotate(360deg); }
}

/* THE STATUS WORD IS A LAYER OVER THE DISC - author ruling 2026-09-05. The
   figure wraps the image rather than positioning against the image zone,
   because the zone is a fixed 100/120/150px on PC and `auto` on the phone
   (the 2026-08-29 ruling): anchoring to the zone would centre the word on
   the zone, not on the disc, and the two only coincide on PC. The figure
   is exactly the disc's box, so the word is centred on the disc wherever
   the disc is.

   pointer-events: none, because the image zone is the modal trigger and a
   click on the word must land on the zone beneath it. The badge does not
   spin - it is a sibling of the disc, not a child, so the animation on
   .wormhole-disc never reaches it. */
.wormhole-figure {
    position: relative;
    display: block;
    width: 50px;
    height: 50px;
}

.wormhole-status {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    white-space: nowrap;
    padding: .1em .5em;
    border-radius: 999px;
    background: hsla(0, 0%, 0%, .6);
    color: hsl(0, 0%, 96%);
    font-size: .6rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    line-height: 1.4;
}

/* Accessibility floor, matching .planet's above: hold a frame rather than
   stopping mid-turn. A rotating disc in the corner of the eye is exactly
   the kind of motion this setting exists to switch off. */
@media (prefers-reduced-motion: reduce) {
    .wormhole-disc {
        animation: none !important;
    }
}

/* ============================================================
   Star sprite sheets - the station, in place of RC_Station.gif
   ============================================================
   Author instruction 2026-09-06. The 1999 station was ALREADY an animated
   GIF - RC_Station.gif, 60 frames at 50x50, 3cs a frame - so the star page
   has always shown it turning. What it could not do was scale: a 50px GIF
   at the travel page's 120px goes soft, and 256 colours with one-bit
   transparency fringe it against the star field. RC_Station.png
   is the same 60 frames at 4x (2000x1200, ten columns of six rows, 200px
   cells) with a real alpha channel, and this draws it the way the suns are
   drawn - by CSS, so it holds a frame under prefers-reduced-motion, which
   an animated GIF cannot be told to do.

   TWO ANIMATIONS WALK THE GRID. background-size is cols x 100% by rows x
   100%, so ONE frame fills the box; background-position in PERCENT then
   lands on frames exactly, because a percentage aligns the same point of
   the image and the box: with the image ten boxes wide, 0% shows column
   0 and 100% column 9, and steps(10, jump-none) visits the ten values
   0, 1/9 ... 1 in ten equal holds. The x animation runs once per row -
   period / rows - and the y animation once per loop, both starting
   together, so frame k is column k mod 10 of row k div 10. Fractional
   cell sizes would work too; they are not needed, the cells are 200px.

   The grid and the file are ALSO named in StarArt's row, and
   QA/check_star_art.php holds the two together - the sheet must divide
   exactly into cols x rows square cells and carry alpha. The first sheet
   supplied had its checkerboard painted in, every pixel opaque.

   NO half-hide and no corona: a station is not a sun. The star page shows
   the whole figure centred as it showed the GIF; the travel page clips
   it at the stage edge the way it clipped the flat file, through the
   translate rules in responsive.css beside .star-jump__flat. */
.star-sprite {
    --size: 60px;
    width: var(--size);
    height: var(--size);
    display: block;
    flex: 0 0 auto;
    background-repeat: no-repeat;
    background-position: 0% 0%;
}

/* Same three buckets as .sun-disc, so a mass-sized cell reads the same
   suffix for a sun and a station. Every station is mass 0.01 today. */
.star-sprite--sun-small  { --size: 60px; }
.star-sprite--sun-medium { --size: 120px; }
.star-sprite--sun-large  { --size: 150px; }

/* THE ORBIT LANE DRAWS THE SAME SHEETS BY FILE. Every station and depot
   star has a companion planet row at orbit 10 (class Station / Depot,
   size 25) whose planets.GFX names the art, and the lane keys on that file
   through StarArt::spriteForFile(). These are .planet's own size buckets
   so the body keeps the size the <img> had; --period is NOT set here,
   the sheet's rule owns it. The modal is .planet--modal's fixed 100px. */
.star-sprite--size0  { --size: 30px; }
.star-sprite--size5  { --size: 30px; }
.star-sprite--size9  { --size: 30px; }
.star-sprite--size10 { --size: 50px; }
.star-sprite--size15 { --size: 50px; }
.star-sprite--size20 { --size: 50px; }
.star-sprite--size25 { --size: 70px; }
.star-sprite--size50 { --size: 100px; }
.star-sprite--modal  { --size: 100px; margin: 0 auto; }

.star-sprite[data-sprite="station"] {
    --period: 3.6s;          /* half the GIF's own speed (1.8s) - author instruction 2026-09-06 */
    background-image: url("/images/RC_Station.png?v=eaa240a0");
    background-size: 1000% 600%;
    animation:
        star-sprite-column calc(var(--period) / 6) steps(10, jump-none) infinite,
        star-sprite-row    var(--period)           steps(6,  jump-none) infinite;
}

/* The depot: RC_FuelD.gif's 30 frames at 4x, six columns of five rows.
   The same two keyframe blocks serve it - they run 0% to 100% whatever
   the grid, and only the step counts and the row-period divisor here
   say how many cells that spans. */
.star-sprite[data-sprite="depot"] {
    --period: 6s;            /* half the GIF's own speed (3.0s) - author instruction 2026-09-06 */
    background-image: url("/images/RC_FuelD.png?v=181b7f05");
    background-size: 600% 500%;
    animation:
        star-sprite-column calc(var(--period) / 5) steps(6, jump-none) infinite,
        star-sprite-row    var(--period)           steps(5, jump-none) infinite;
}

/* The asteroid: RC_asteroid1a.gif's 30 frames at 8x, six columns of five
   rows. By colour it is the one 'none' star, Sargasso Sector; by file it
   is every Asteroid Belt planet row in the galaxy, 5,358 of them, at the
   orbit lane's own size buckets. */
.star-sprite[data-sprite="asteroid"] {
    --period: 3s;            /* the GIF's own loop: 30 frames x 10cs */
    background-image: url("/images/RC_Asteroid.png?v=7cb8373d");
    background-size: 600% 500%;
    animation:
        star-sprite-column calc(var(--period) / 5) steps(6, jump-none) infinite,
        star-sprite-row    var(--period)           steps(5, jump-none) infinite;
}

/* The outpost: an orbit FEATURE with no star colour behind it - a planets
   row of class Outpost, reached by file (StarArt::SHEETS). 72 frames on a
   12 x 6 grid of exact 85px cells, DS_Outpost.gif's 3.8s turn. */
.star-sprite[data-sprite="outpost"] {
    --period: 3.8s;          /* the GIF's own loop: 25 frames x 15cs */
    background-image: url("/images/DS_Outpost.png?v=2fd86250");
    background-size: 1200% 600%;
    animation:
        star-sprite-column calc(var(--period) / 6) steps(12, jump-none) infinite,
        star-sprite-row    var(--period)           steps(6,  jump-none) infinite;
}

@keyframes star-sprite-column {
    from { background-position-x: 0%; }
    to   { background-position-x: 100%; }
}
@keyframes star-sprite-row {
    from { background-position-y: 0%; }
    to   { background-position-y: 100%; }
}

/* Accessibility floor, matching .sun-disc and .wormhole-disc: hold frame
   0 rather than stop mid-turn. */
@media (prefers-reduced-motion: reduce) {
    .star-sprite {
        animation: none !important;
    }
}

/* THE SHOP FRONTS - dossier 17 step 3, 2026-09-07. DS_Shops.jpg is a
   CATALOGUE sheet: 8 columns by 6 rows of 178 x 216 cells, one shop per
   cell, exported from DS_Shops_Master.png. Not an animation - nothing walks
   the grid; the page picks ONE cell by number. shops.CELL is that number
   for a real shop and Planets_Read_Shops carries a literal for a synthetic
   one; app/Helpers/ShopArt.php turns it into --col and --row, and this rule
   does the rest at a scale --s the page sets (the planet page draws at .4
   on a phone, 71 x 86, the size docs/shop-spritesheet-prompt.md asked every
   cell to read at; the shop room draws at 1, the cell's own size). The
   geometry here and in ShopArt are held equal by QA/check_star_art.php.

   IT DECLARES NEITHER --s NOR --col/--row, AND THAT IS A FIX, not a style -
   author instruction 2026-09-07. This rule used to assign `--s: 1`, and
   `.shop-page__front { --s: .6 }` in responsive.css then LOST to it: both
   name one class, so the tie broke on source order, and common_head_tags.php
   emits responsive.css BEFORE layout.app.php emits this file. Measured at
   both widths - the shop room's front drew 178 x 216 where 107 x 130 was
   asked for, on every screen. The room went back to full size a day
   later by author instruction, so what that fix bought is a caller whose
   scale is now obeyed - see the note above .shop-page__front.

   `.planet-shop .shop-front` WAS NEVER AFFECTED, and the difference is the
   whole lesson: that caller names TWO classes, so it wins on specificity
   and never had to care about order. One selector written with a parent
   class is all that separated a rule that worked from one that did not.

   Reading with a var() fallback leaves nothing to lose to, whichever file
   is read last and however many classes a future caller names. The two
   ship sheets below were changed the same way, the same day, for the same
   reason. */
.shop-front {
    display: block;
    width: calc(178px * var(--s, 1));
    height: calc(216px * var(--s, 1));
    background-image: url("/images/DS_Shops.jpg?v=65b268b1");
    background-repeat: no-repeat;
    background-size: calc(1424px * var(--s, 1)) calc(1296px * var(--s, 1));
    background-position: calc(var(--col, 0) * -178px * var(--s, 1)) calc(var(--row, 0) * -216px * var(--s, 1));
}

/* THE COMMODITIES - 2026-09-10. DS_Commodities.jpg is the shop sheet's
   twin: a CATALOGUE of 8 columns by 11 rows of 178 x 216 cells, one
   commodity per cell and courier freight at cell 80, exported from
   DS_Commodities_Master.png.
   commodities.CELL is the number; app/Helpers/CommodityArt.php turns it into
   --col and --row, and this rule does the rest at a scale --s the caller
   sets. The geometry here and in CommodityArt are held equal by
   QA/check_commodity_art.php.

   IT DECLARES NEITHER --s NOR --col/--row, for the reason written above
   .shop-front: responsive.css loads BEFORE this file, so a declaration here
   would beat a one-class caller on source order and the caller's scale
   would be silently ignored. Read with a fallback, there is nothing to
   lose to.

   NO max-width ON A CALLER. The sheet is stepped in pixels, so a clamp
   CROPS the cell instead of scaling it - the ship sheet's lesson. Set --s. */
.commodity-art {
    display: block;
    width: calc(178px * var(--s, 1));
    height: calc(216px * var(--s, 1));
    background-image: url("/images/DS_Commodities.jpg?v=3f5f7f50");
    background-repeat: no-repeat;
    background-size: calc(1424px * var(--s, 1)) calc(2376px * var(--s, 1));
    background-position: calc(var(--col, 0) * -178px * var(--s, 1)) calc(var(--row, 0) * -216px * var(--s, 1));
}

/* THE TWO SHIP SHEETS - app/Helpers/ShipArt.php is the one description of
   both. 6 columns by 6 rows of 264 x 165 cells, 34 drawn; the view writes
   --col and --row and the CLASS picks the picture.

   STEPPED IN PERCENT, NOT PIXELS, AND THAT IS WHAT MAKES IT RESIZABLE.
   background-size: 600% 600% makes ONE CELL FILL THE BOX whatever the box
   is, and a percentage background-position aligns the same point of image
   and box - so cell k sits at k * 20% (six columns, five box-widths of
   travel). The station sheet already works this way; see .star-sprite.

   IT REPLACED A PIXEL SCALE ON 2026-09-07, AND THAT WAS A LIVE BUG, not a
   tidy-up. The rule used to declare `--s: 1` and compute width from it, so
   every page that set its own --s LOST: .star-jump__ship's `--s: .38` and
   .negotiation__ship's `width: 139px` both live in responsive.css, which
   common_head_tags.php emits BEFORE layout.app.php emits this file - equal
   specificity, later wins. Measured: the jump ship rendered 264x165 where
   100x63 was intended. The same trap responsive.css has now swallowed four
   declarations with.

   SO NOTHING HERE IS DECLARED THAT A PAGE MIGHT WANT TO SET. --ship-w,
   --col and --row are read with var() fallbacks and never assigned, so a
   caller's value cannot be beaten by source order - there is no competing
   declaration to lose to. Height follows from aspect-ratio, so a page
   states ONE number.

   `max-width: 100%` ON A CALLER NOW DOES WHAT ITS COMMENT ALWAYS CLAIMED:
   with aspect-ratio the height follows the clamped width, where the pixel
   form cropped the cell instead. */
.ship-side,
.ship-portrait {
    display: block;
    width: var(--ship-w, 264px);
    aspect-ratio: 264 / 165;
    background-repeat: no-repeat;
    background-size: 600% 600%;
    background-position: calc(var(--col, 0) * 20%) calc(var(--row, 0) * 20%);
}

/* THE PORTRAIT IS THE 3/4 VIEW - author ruling 2026-09-07: it is drawn
   wherever a ship's PICTURE is shown. The side view is the profile, for a
   vessel seen PASSING.

   WHO TAKES WHICH, as of 2026-09-08:
     portrait   the ship scan card, the pirate hold-up
     side       the star page's orbit lane, the travel page

   THE HOLD-UP MOVED HERE on 2026-09-08 (author ruling) - this comment used
   to name it as a side-view caller. Its plate is a picture, the thug's face
   beside his hull, both held still while he makes his demand; nothing is in
   motion. check_ship_art.php pins the sheet per file, and pins that no page
   carries BOTH classes - the shape a half-done change leaves behind. */
.ship-portrait { background-image: url("/images/RC_Ships.png?v=13c74b06"); }
.ship-side     { background-image: url("/images/RC_Ships_Side.png?v=4963f155"); }
