@charset "UTF-8";
/* ==========================================================================
   FISHER — design system (light)
   --------------------------------------------------------------------------
   One stylesheet. No framework, no build step.

   THE LOOK: a printed field guide.

   Think the plate pages of a good natural-history book — warm paper, deep
   teal ink, generous margins, and photographs that get to be the loudest
   thing on the page. Every decision follows from that:

   · Paper, not white. #faf7f1 rather than #ffffff. Pure white glares and
     makes photographs look grey; warm paper makes them look printed.
   · Ink, not black. Body text is a very dark desaturated teal, which sits
     with the sea colours instead of fighting them.
   · Depth comes from shadow, not from borders. Cards float on the page with
     soft, warm-tinted shadows and a hairline edge — the way a print sits on
     a mount board.
   · One warm accent, used once. Gold means "record". Nothing else on the
     page is warm, so the eye finds it instantly.
   · Water is present but quiet: a wash at the top of the page, a paper
     grain over everything, and caustics only in the hero.

   TYPE: Fraunces for display — a variable serif with real optical sizing, so
   headlines are tight and editorial while small text stays readable. Outfit
   for interface text. Both carry full Latin Extended, so Turkish ğ ş ı İ ö ü ç
   render natively rather than falling back mid-word.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
/* ==========================================================================
   1. TOKENS & THEMES
   --------------------------------------------------------------------------
   Colour is a choice rather than a decision baked into the stylesheet. Every
   component references tokens only, so switching is a single attribute on
   <html> and nothing else in the file changes.

   Each theme defines the same contract:

     paper / card        page and raised surfaces
     ink*                text, four weights
     line*               rules and borders
     w-050 … w-900       the water ramp, light to deep. Drives depth bands,
                         map tints, chart grounds. Must run light→dark.
     sea*                the interactive colour: links, controls, focus
     mark*               reserved for records and personal bests, nothing else
     rust / clay / moss  status colours (warning, danger, safe)

   TWO THEMES, and the test they have to pass.

   There were six once. Four were variations on "a tasteful light grey" and
   nobody was ever going to pick between them, so they went — every extra
   palette is another set of colour rules to keep true forever, and the ones
   that got cut were the ones with nothing to say. Chart and Abyss were tried
   against that rule and cut for the same reason; Harbor Light was built,
   shipped and then removed. They are all in the history if they are wanted
   back.

     SPECTRUM   the daylight reef. Seven encoded hues, and the default.
     HARBOUR    the dark one. Deep's blue brought up toward the surface, with
                the ink ramp in white rather than tinted toward the page —
                a blue-grey on a blue ground has nowhere to go, which is what
                made the quiet text on the darker draft hard to read.

   HARBOUR IS WHERE "DEEP" USED TO BE, and that is deliberate rather than a
   rename for its own sake. The palette is only half of what a dark theme is
   here: the map picks its dark tiles off the theme name, and the brand word,
   the medals, the rank badges, the record bars, the depth-band labels and the
   three aquarium depth layers all have their own [data-theme] rules that have
   nothing to do with tokens. Deleting Deep and adding Harbor beside it would
   have stranded every one of those and left the dark theme rendering in light
   -theme dress. So Deep's selector became Harbor's and carried them across.

   Spectrum is bound to :root as well as to its own attribute, so the page
   still has a complete palette if the theme attribute is ever missing or
   holds a value that no longer exists.
   ========================================================================== */

/* ---- SPECTRUM — the daylight reef (default) -----------------------------
   Every hue encodes something. Depth zones run violet at the surface to deep
   blue in the abyss; water types, trophy tiers and conservation status each
   own a slot. If two things are different colours here, they are different
   things — that discipline is what stops a seven-hue palette becoming a
   clown car.
   ------------------------------------------------------------------------ */
:root,
[data-theme="spectrum"] {
    --paper:       #f6f8fa;
    --paper-warm:  #eef2f6;
    --paper-sunk:  #dfe7ee;
    --card:        #ffffff;

    /* Water ramp keeps a blue spine so the page still reads as water */
    --w-050: #ecf6fb;
    --w-100: #d6ebf6;
    --w-200: #aed8ec;
    --w-300: #78bcdd;
    --w-400: #4098c4;
    --w-500: #1f76a5;
    --w-600: #145c85;
    --w-700: #0e4566;
    --w-800: #0a2f47;
    --w-900: #061d2e;

    --ink:       #0a1922;
    --ink-soft:  #2e4653;
    --ink-mute:  #5b7380;
    --ink-faint: #8a9ca8;

    /* PLATE STOCK — the ground a species illustration sits on.
       It was warm off-white, the natural-history convention of a fish printed
       on a page. It is water now: the fish is drawn in the element it lives
       in, which is what the rest of this site already does with every page
       behind it.

       Built from the page's own water ramp and following its rule — bright at
       the surface, deeper toward the floor — so the plate reads as a piece of
       the same sea rather than a blue panel dropped onto it. Two stops and
       nothing else: no rays, no bubbles, no motion.

       Still a background image, never a colour, so the one thing to keep in
       mind is that --plate-paper can only go in `background`.

       The rule and ink moved with it, because the beige hairline and warm grey
       were chosen to sit with cream and read as out of place beside blue. The
       caption itself is NOT on the water — it follows the </figure> and sits
       on the page underneath — so its ink is the deep end of the same ramp for
       the palette's sake, at 10:1 on a card. */
    --plate-paper: linear-gradient(180deg, var(--w-100) 0%, var(--w-300) 100%);
    --plate-rule:  var(--w-200);
    --plate-ink:   var(--w-700);

    --line:           rgba(13, 31, 44, .12);
    --line-strong:    rgba(13, 31, 44, .26);
    --surface:        rgba(13, 31, 44, .035);
    --surface-raised: rgba(13, 31, 44, .065);

    --sea:        #0a6e9e;
    --sea-bright: #1a8cc0;
    --sea-deep:   #06496d;
    --sea-wash:   #e4eff6;

    --mark:       #a8456b;
    --mark-bright:#c56287;
    --mark-wash:  #f6e9ee;

    --rust:       #a85a28;
    --clay:       #b03d47;
    --moss:       #1a8055;
    --sand-deep:  #7d671a;
}

/* ---- DEEP — dark navy, photographs glow against it ---------------------- */
[data-theme="harbor"] {
    /* SEPARATION WAS THE REAL PROBLEM, not the individual colours.
     *
     * Every value on this theme had been derived to CLEAR 4.5:1 and stop
     * there. That is the floor for legibility and a poor target for anything
     * else: it makes a palette where nothing is unreadable and nothing stands
     * out. Measured, the first version had cards separating from the page by
     * 1.21, hairlines at 1.54, and six accents parked between 4.60 and 4.64 —
     * which is exactly what "blends into the background" looks like as a set
     * of numbers.
     *
     * The page is a shade deeper and the card a shade lighter, so a panel now
     * lifts off the ground at 1.53 instead of 1.21. Text on it is still strong
     * — white at 8.37:1 — because the card moved toward the ink, not away. */
    --paper:       #1d3448;
    --paper-warm:  #213c53;
    --paper-sunk:  #26445e;
    --card:        #2e516c;

    --w-050: #20394f;
    --w-100: #26445e;
    --w-200: #2d5170;
    --w-300: #376389;
    --w-400: #457baa;
    --w-500: #6294c0;
    --w-600: #86adce;
    --w-700: #abc6dd;
    --w-800: #cfdeec;
    --w-900: #f3f7fa;

    /* White, then steps toward it with the saturation pulled out. A blue-grey
       on a blue ground has nowhere to go — that is why the quiet text read as
       dim on the first draft rather than merely quiet. */
    --ink:       #ffffff;
    --ink-soft:  #e4edf4;
    --ink-mute:  #c6d7e2;
    --ink-faint: #adc1cf;

    /* Water here too, on harbor's own ramp: lit at the surface, deepening to
       the floor. The top stop is lighter than the old flat #396485 and the
       bottom darker, so it still separates from the card it sits on while
       reading as depth rather than as a lit case. The caption sits below the
       plate on the page, not on this, so its pale ink is unchanged. */
    --plate-paper: linear-gradient(180deg, var(--w-400) 0%, var(--w-200) 100%);
    --plate-rule:  rgba(206, 228, 242, .41);
    --plate-ink:   #c6d7e2;

    /* Hairlines you can see. These were composited at 1.54 against the card,
       which is a border in the stylesheet and nothing on the screen. */
    --line:           rgba(206, 228, 242, .35);
    --line-firm:      rgba(206, 228, 242, .57);
    --surface:        rgba(206, 228, 242, .10);
    --surface-raised: rgba(206, 228, 242, .17);

    /* SATURATED FIRST, THEN LIFTED ONLY AS FAR AS THE TARGET NEEDS.
     *
     * The obvious fix for an accent that blends is to make it brighter, and it
     * is the wrong one: raising luminance alone walks a colour toward white and
     * takes its identity with it. Pushed to 6.2:1 that way, --mark came out
     * #f0dbbd, which is beige, not gold. Chroma is what separates a colour from
     * a dark ground. So each of these is saturated by about a third first and
     * then lifted only to 5.6:1 — a full point above where they sat, while
     * still reading as sea, gold, moss, clay and rust. */
    --sea:        #addaf5;
    --sea-bright: #c9eafc;
    --sea-deep:   #5bb5eb;
    --sea-wash:   rgba(173, 218, 245, .22);

    --mark:       #f7cd91;
    --mark-bright:#f9daae;
    --mark-wash:  rgba(247, 205, 145, .22);

    --moss:       #aadfc4;
    --clay:       #f4cacc;
    --rust:       #f3ccb8;
    --sand-deep:  #e6d2a7;
}


/* The seven hue slots. On Deep they resolve to tints of its own accents, so
   every hue-coded component keeps working on a dark palette without the page
   turning into a rainbow. Spectrum overrides them below with the real thing. */
:root, [data-theme] {
    --h1: var(--sea-deep);   --h1-wash: var(--sea-wash);
    --h2: var(--sea);        --h2-wash: var(--sea-wash);
    --h3: var(--sea-bright); --h3-wash: var(--sea-wash);
    --h4: var(--moss);       --h4-wash: var(--surface-raised);
    --h5: var(--sand-deep);  --h5-wash: var(--surface-raised);
    --h6: var(--rust);       --h6-wash: var(--mark-wash);
    --h7: var(--mark);       --h7-wash: var(--mark-wash);
}

/* ---- The spectrum itself ------------------------------------------------
   Seven reef hues at matched lightness and saturation, so none shouts louder
   than the rest. Sampled from a coral reef rather than spun off a hue wheel,
   which is why they sit together instead of fighting.

   Declared after the fallback block above so it wins — same specificity,
   later in the file.
   ------------------------------------------------------------------------ */
[data-theme="spectrum"] {
    --h1: #6247ad;   /* violet   — royal gramma   */
    --h2: #35659f;   /* blue     — blue tang      */
    --h3: #12708a;   /* cyan     — surgeonfish    */
    --h4: #1a8163;   /* green    — parrotfish     */
    --h5: #7d671a;   /* yellow   — butterflyfish  */
    --h6: #a85a28;   /* orange   — clownfish      */
    --h7: #a8456b;   /* coral    — anthias        */

    --h1-wash: #efe9fc;
    --h2-wash: #e6eefc;
    --h3-wash: #ddf2f9;
    --h4-wash: #ddf5ee;
    --h5-wash: #faf2d8;
    --h6-wash: #fcece2;
    --h7-wash: #fde8f0;
}

/* Shared derived tokens. Declared after the themes so every palette gets
   them, and overridden below only where a theme genuinely differs. */
:root, [data-theme] {
    /* The record colour used to be called "gold"; it is now --mark, because
       on Spectrum it is coral, not gold. Aliases keep old rules working. */
    --gold:        var(--mark);
    --gold-bright: var(--mark-bright);
    --gold-wash:   var(--mark-wash);
    --sand:        var(--sand-deep);

    /* Shadows carry the theme's own darkness rather than neutral black —
       a grey shadow on a warm page always looks like a mistake. */
    --shadow-rgb: 15, 28, 38;

    /* Graticule line weights */
    --grid-1: rgba(15, 28, 38, .05);
    --grid-2: rgba(15, 28, 38, .08);
}

/* Dark themes invert what a "line over the page" has to be */
[data-theme="harbor"] {
    --shadow-rgb: 0, 0, 0;
    --grid-1: rgba(174, 205, 226, .05);
    --grid-2: rgba(174, 205, 226, .08);
}

/* A dark theme, so the rules go light over the page like Deep's — and a touch
   stronger than Deep's, because a lifted ground gives a pale hairline less to
   push against. */




:root {

    /* Type */
    /* ONE FAMILY, TWO ROLES — the serif/sans pairing is gone.
     *
     * Fraunces set the old register: a variable serif with optical sizing is a
     * PRINT voice, and it is what made every page read as a field guide. The
     * product this is now — a logbook you open on a phone, a board you check
     * after a session — does not want a print voice, and a display serif over
     * dense tabular data is the single loudest thing on any screen it appears.
     *
     * Outfit carries both roles because a geometric sans at 800
     * IS the display treatment in this register; contrast comes from weight
     * and size rather than from a second family. One family is also one font
     * request instead of two, and it keeps Latin Extended for Turkish — ğ ş ı
     * İ ö ü ç render natively, which was the constraint that picked the last
     * pair too. It was already the body font, so this costs no new asset
     * and keeps the website and the Flutter app on one family. */
    --font-display: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    --font-body:    "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

    /* THE WORDMARK KEEPS THE SERIF, and it is the only thing that does.
       See .brand-word — a logo is not UI, and swapping it is rebranding rather
       than redesigning. Fraunces is still loaded for this one rule. */
    --font-mark:    "Fraunces", "Iowan Old Style", Georgia, serif;
    --font-mono:    ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;

    /* An editorial scale: small text stays small and readable, display type
       is allowed to get genuinely large. The jump between --t-lg and --t-xl
       is deliberately wide — timid headline sizes are what make a layout
       read as a template. */
    --t-xs:   .75rem;
    --t-sm:   .8125rem;
    --t-base: .9375rem;
    --t-md:   1.0625rem;
    --t-lg:   clamp(1.25rem, 1rem + .7vw, 1.6rem);
    --t-xl:   clamp(1.75rem, 1.2rem + 2.2vw, 2.9rem);
    --t-2xl:  clamp(2.4rem, 1.4rem + 4.2vw, 4.6rem);
    /* CUT FROM 7rem. That ceiling was set for a 400-weight serif, where a huge
       size is what carried the headline. At 800 the same size is roughly twice
       the ink on screen and the hero title wrapped to five lines. */
    --t-3xl:  clamp(2.6rem, 1.4rem + 3.6vw, 4.4rem);

    --gap-sm: .75rem;
    --gap:    1.25rem;
    --gap-lg: 2rem;
    --pad:    clamp(1rem, 3vw, 2.25rem);
    --shell:  1320px;
    --shell-narrow: 780px;

    /* RADII CARRY MOST OF THE NEW REGISTER. A 14px corner reads as a document;
       an 18-26px corner reads as a surface, which is the difference between a
       page and an app. --r-pill is new and is what buttons and nav now use. */
    --r-sm:   10px;
    --r:      18px;
    --r-lg:   26px;
    --r-pill: 999px;
    --r-xl: 28px;

    /* Shadows are warm and low-contrast — cool grey shadows on warm paper
       look like a mistake. */
    /* FLATTER AND TIGHTER. The old shadows were print shadows — a wide, soft
       cast, the way a photograph sits on a mount board. A product surface sits
       ON the page rather than above it, so these are short and close, and the
       large ambient blur is gone from every level but the top one. */
    --shadow-sm: 0 1px 2px rgba(var(--shadow-rgb), .06);
    --shadow:    0 1px 2px rgba(var(--shadow-rgb), .06), 0 4px 12px rgba(var(--shadow-rgb), .06);
    --shadow-lg: 0 2px 6px rgba(var(--shadow-rgb), .07), 0 18px 44px rgba(var(--shadow-rgb), .12);

    --ease:      cubic-bezier(.22, .61, .36, 1);
    --ease-back: cubic-bezier(.34, 1.32, .64, 1);
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scrollbar-color: var(--w-300) var(--paper-warm);
    color-scheme: light;
}

body {
    font-family: var(--font-body);
    font-size: var(--t-base);
    line-height: 1.6;
    color: var(--ink);
    background: var(--paper);
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    overflow-x: hidden;
}

img, svg, video { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; padding: 0; }
table { border-collapse: collapse; width: 100%; }
figure { margin: 0; }

:focus-visible { outline: 2px solid var(--sea); outline-offset: 3px; border-radius: 4px; }
::selection { background: var(--w-200); color: var(--ink); }

/* --------------------------------------------------------------------------
   3. The page surface
   A sea wash at the top, a paper grain over everything. The grain is the
   quiet hero — it's what stops a large flat cream area from looking like an
   unstyled div.
   -------------------------------------------------------------------------- */
.water {
    position: fixed;
    inset: 0;
    z-index: -3;
    background:
        radial-gradient(130% 60% at 50% -10%, var(--w-100) 0%, transparent 64%),
        linear-gradient(180deg, var(--paper) 0%, var(--paper) 62%, var(--paper-warm) 100%);
}

/* The water column. Bright at the surface, deepening toward the floor — the
   page is inside the tank, so the background has to behave like water rather
   than like paper. Iridescence at the top is the oil-on-water sheen, kept to
   a few percent: felt at the edge of vision, not looked at. */
[data-theme="spectrum"] .water {
    background:
        radial-gradient(90% 50% at 8% 0%,   rgba(125, 92, 214, .14) 0%, transparent 58%),
        radial-gradient(80% 45% at 42% -6%, rgba(14, 159, 196, .16) 0%, transparent 60%),
        radial-gradient(85% 45% at 78% 2%,  rgba(23, 169, 126, .13) 0%, transparent 58%),
        radial-gradient(70% 40% at 96% 12%, rgba(224, 115, 58, .11) 0%, transparent 60%),
        linear-gradient(180deg,
            var(--w-050) 0%,
            var(--paper) 26%,
            var(--w-100) 68%,
            var(--w-200) 100%);
}

/* Every theme becomes a water column now, just at its own intensity */
.water {
    background:
        radial-gradient(130% 55% at 50% -10%, var(--w-100) 0%, transparent 62%),
        linear-gradient(180deg,
            var(--paper) 0%,
            var(--paper) 40%,
            color-mix(in srgb, var(--w-100) 55%, var(--paper)) 100%);
}

/* Deepens toward the floor exactly as the tank should — this is still a water
   column, just one lit from nearer the surface. */

[data-theme="harbor"] .water {
    background:
        radial-gradient(120% 55% at 50% -8%, rgba(169, 213, 237, .11) 0%, transparent 62%),
        linear-gradient(180deg, var(--paper) 0%, var(--paper-warm) 45%, var(--paper-sunk) 100%);
}

/* The graticule — the lat/long grid ruled across every chart.
   It replaced a film-grain overlay, and it does the same job of stopping
   large flat areas looking like unstyled divs, except it means something.
   Two weights, exactly as on a chart: a fine minute grid and a heavier
   degree line every fifth cell. */
.water::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: .55;
    background-image:
        linear-gradient(var(--grid-1) 1px, transparent 1px),
        linear-gradient(90deg, var(--grid-1) 1px, transparent 1px),
        linear-gradient(var(--grid-2) 1px, transparent 1px),
        linear-gradient(90deg, var(--grid-2) 1px, transparent 1px);
    background-size: 28px 28px, 28px 28px, 140px 140px, 140px 140px;
    animation: none;
}

/* A little paper tooth over the top, so the buff reads as printed stock
   rather than a flat fill. Much lighter than the old grain — the graticule
   is now doing most of the work. */
.water::after {
    content: "";
    position: absolute;
    inset: 0;
    opacity: .3;
    pointer-events: none;
    mix-blend-mode: multiply;
    background-image: 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='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.24'/%3E%3C/svg%3E");
}

/* ==========================================================================
   3b. THE AQUARIUM
   --------------------------------------------------------------------------
   The whole page sits inside a tank. Water column, light shafts, fish
   crossing at several depths, a reef along the floor, bubbles rising.

   PERFORMANCE. A full-screen animated background is the easiest way to make a
   site feel slow, so this one obeys three rules:

     · only `transform` and `opacity` animate — both composite on the GPU and
       skip layout and paint entirely
     · filters (blur, saturate) are applied only to STATIC layers, rasterised
       once; a blur on a moving element re-rasterises every frame
     · ~26 animated nodes total, no JavaScript, no canvas, no rAF loop

   The compositor runs the whole thing on its own thread, so it keeps moving
   smoothly even while the main thread renders a 250-row table.
   ========================================================================== */
.aq {
    position: fixed;
    inset: 0;
    z-index: -2;
    overflow: hidden;
    pointer-events: none;

    --aq-sand-top:    color-mix(in srgb, var(--w-200) 60%, #f0e4c8);
    --aq-sand-bottom: color-mix(in srgb, var(--w-400) 45%, #d8c39a);
    --aq-shadow:      var(--w-900);
}

.aq-defs { position: absolute; width: 0; height: 0; }

/* ---- Fish --------------------------------------------------------------
   Absolutely positioned so the swim can be a percentage translate, which is
   relative to the element and therefore cheap. Each is its own compositor
   layer and never triggers layout. */
.aq-school { position: absolute; inset: 0; }

.aq-fish {
    position: absolute;
    left: 0;
    height: auto;
    fill: currentColor;
    stroke: currentColor;
    stroke-width: 0;
    animation-name: aq-swim;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    will-change: transform;
}

/* Right to left across the viewport.
   
   NO scaleX(-1) any more, and its removal is the whole fix for fish that swam
   backwards. The old hand-drawn symbols were head-RIGHT — tail at x=2, snout at
   the far end — so travelling leftward meant mirroring them. FishArt draws
   every fish head-LEFT, which is the orientation the species pages and the
   certificates use, so the mirror that used to correct the blobs now reverses
   the real ones: a marlin crossing the screen bill-first backwards. */
@keyframes aq-swim {
    from { transform: translateX(105vw); }
    to   { transform: translateX(-20vw); }
}

/* ---- Reef floor --------------------------------------------------------- */
.aq-reef {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: clamp(190px, 30vh, 330px);
    display: block;
}

/* Depth cueing, applied once to static layers. Distance in water desaturates
   and softens — this is what gives the scene depth rather than making it look
   like stickers on glass. */
.aq-far  { opacity: .28; filter: saturate(.4) blur(1.6px); }
.aq-mid  { opacity: .5;  filter: saturate(.75) blur(.4px); }
.aq-near { opacity: .72; }

/* Soft corals move; hard corals do not, because they can't */
.aq-sway {
    transform-box: fill-box;
    transform-origin: bottom center;
    animation: aq-sway 9s ease-in-out infinite;
}
.aq-sway:nth-of-type(3n)   { animation-duration: 12s; animation-delay: -4s; }
.aq-sway:nth-of-type(3n+1) { animation-duration: 7.5s; animation-delay: -2s; }

@keyframes aq-sway {
    0%, 100% { transform: rotate(-2.5deg); }
    50%      { transform: rotate(3deg); }
}

/* ---- Bubbles ------------------------------------------------------------ */
.aq-bubbles { position: absolute; inset: 0; }
.aq-bubbles i {
    position: absolute;
    bottom: -3%;
    border-radius: 50%;
    border: 1px solid color-mix(in srgb, var(--w-500) 40%, transparent);
    background: radial-gradient(circle at 34% 30%,
                rgba(255, 255, 255, .85), color-mix(in srgb, var(--w-200) 30%, transparent) 62%, transparent);
    animation-name: aq-rise;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    will-change: transform, opacity;
}

@keyframes aq-rise {
    0%   { transform: translate3d(0, 0, 0) scale(.6); opacity: 0; }
    8%   { opacity: .55; }
    50%  { transform: translate3d(18px, -52vh, 0) scale(1); }
    92%  { opacity: .4; }
    100% { transform: translate3d(-10px, -105vh, 0) scale(1.1); opacity: 0; }
}

/* ---- Sunlight ----------------------------------------------------------- */
.aq-rays { position: absolute; inset: 0; }
.aq-rays span {
    position: absolute;
    top: -30%;
    width: 16vw;
    height: 150%;
    background: linear-gradient(180deg,
                color-mix(in srgb, #fff 26%, transparent), transparent 72%);
    transform-origin: top center;
    animation: aq-shaft 16s ease-in-out infinite;
    will-change: opacity;
}
.aq-rays span:nth-child(1) { left: 8%;  rotate: 8deg; }
.aq-rays span:nth-child(2) { left: 44%; rotate: -6deg; animation-delay: -6s; }
.aq-rays span:nth-child(3) { left: 76%; rotate: 10deg; animation-delay: -11s; }

@keyframes aq-shaft {
    0%, 100% { opacity: .35; }
    50%      { opacity: .8; }
}

/* ---- Per-theme intensity -----------------------------------------------
   On the dark theme the tank is the brightest thing on screen and has to be
   pulled back hard; the monochrome themes want a watermark, not a mural. */
/* THE SPECIES COLOUR, LIFTED FOR A DARK GROUND.
 *
 * Each fish now carries its own accent as --fish, set inline from the species
 * row: a mahi is gold, a grouper brown, a tuna deep blue. Those span nearly
 * the whole lightness range, which is fine on paper and useless on navy —
 * #1E3F6B on #1D3448 is not a fish, it is a rumour.
 *
 * So the hue is kept and the lightness is lifted, the same correction reef.dart
 * makes for the app. Mixing toward a pale blue rather than pure white keeps the
 * water in it. */
[data-theme="harbor"] .aq-fish { color: color-mix(in srgb, var(--fish) 42%, #e6f3ff); }

[data-theme="harbor"] .aq-far  { opacity: .2; }
[data-theme="harbor"] .aq-mid  { opacity: .34; }
[data-theme="harbor"] .aq-near { opacity: .5; }
[data-theme="harbor"] .aq-rays span { background: linear-gradient(180deg, rgba(120, 200, 255, .16), transparent 70%); }


/* Phones: fewer moving parts and a shorter reef. The fish are the first
   thing to go — they are the most animated and the least legible at that
   size. */
@media (max-width: 700px) {
    .aq-reef { height: 150px; }
    .aq-school .aq-fish:nth-child(n+5) { display: none; }
    .aq-bubbles i:nth-child(n+5) { display: none; }
}

/* Caustics — hero only, and barely there */
.rays {
    position: absolute;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    mask-image: linear-gradient(180deg, #000 0%, transparent 82%);
}
.rays span {
    position: absolute;
    top: -35%;
    width: 16vw;
    height: 170%;
    background: linear-gradient(180deg, rgba(93, 169, 164, .14), transparent 70%);
    transform-origin: top center;
    filter: blur(30px);
    animation: sway 20s ease-in-out infinite;
}
.rays span:nth-child(1) { left: 5%;  transform: rotate(9deg); }
.rays span:nth-child(2) { left: 32%; transform: rotate(-6deg); animation-delay: -6s; opacity: .7; }
.rays span:nth-child(3) { left: 61%; transform: rotate(12deg); animation-delay: -11s; opacity: .55; }
.rays span:nth-child(4) { left: 85%; transform: rotate(-4deg); animation-delay: -15s; opacity: .65; }

@keyframes sway {
    0%, 100% { opacity: .4; }
    50%      { opacity: .85; }
}

.bubbles { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
.bubbles i {
    position: absolute;
    bottom: -20px;
    border-radius: 50%;
    border: 1px solid rgba(47, 138, 134, .28);
    background: radial-gradient(circle at 34% 30%, rgba(255, 255, 255, .9), rgba(191, 222, 219, .3) 62%, transparent);
    animation: drift-up linear infinite;
}
.bubbles i:nth-child(1) { left: 11%; width: 5px; height: 5px; animation-duration: 26s; }
.bubbles i:nth-child(2) { left: 27%; width: 3px; height: 3px; animation-duration: 34s; animation-delay: -9s; }
.bubbles i:nth-child(3) { left: 46%; width: 7px; height: 7px; animation-duration: 30s; animation-delay: -18s; }
.bubbles i:nth-child(4) { left: 64%; width: 4px; height: 4px; animation-duration: 38s; animation-delay: -5s; }
.bubbles i:nth-child(5) { left: 79%; width: 6px; height: 6px; animation-duration: 28s; animation-delay: -22s; }
.bubbles i:nth-child(6) { left: 92%; width: 3px; height: 3px; animation-duration: 42s; animation-delay: -13s; }

@keyframes drift-up {
    0%   { transform: translateY(0) translateX(0); opacity: 0; }
    12%  { opacity: .7; }
    50%  { transform: translateY(-45vh) translateX(22px); }
    88%  { opacity: .4; }
    100% { transform: translateY(-100vh) translateX(-14px); opacity: 0; }
}

/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */
.shell { width: min(100% - 2 * var(--pad), var(--shell)); margin-inline: auto; }
.shell-narrow { width: min(100% - 2 * var(--pad), var(--shell-narrow)); margin-inline: auto; }

.section { padding-block: clamp(3rem, 7vw, 6rem); position: relative; }
.section-tight { padding-block: clamp(2rem, 4vw, 3.25rem); }

/* THREE STACK SIZES, AND .stack-sm WAS THE ONE THAT DID NOT EXIST.
   It is used in twenty places — the password form, the avatar and banner
   forms, the privacy sections, and the account deletion form — and every one
   of them was rendering with no vertical rhythm at all, because a class that
   is never declared is not an error anywhere. The visible symptom was the
   "delete my account permanently" button sitting flush against the input
   above it, with the confirmation gate and the irreversible control touching.
   Declaring it here fixes all twenty at once, which is the point of having
   the utility in the first place. */
.stack > * + * { margin-top: var(--gap); }
.stack-sm > * + * { margin-top: var(--gap-sm); }
.stack-lg > * + * { margin-top: var(--gap-lg); }

.row { display: flex; gap: var(--gap); flex-wrap: wrap; align-items: center; }
.row-between { justify-content: space-between; }
.row-end { justify-content: flex-end; }
.row-tight { gap: .5rem; }

.grid { display: grid; gap: var(--gap); }
.grid-auto { grid-template-columns: repeat(auto-fill, minmax(min(272px, 100%), 1fr)); }
.grid-wide { grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(310px, 100%), 1fr)); }

.sidebar-layout { display: grid; gap: var(--gap-lg); grid-template-columns: minmax(0, 1fr); }
@media (min-width: 980px) {
    .sidebar-layout { grid-template-columns: 268px minmax(0, 1fr); align-items: start; }
    .sidebar-layout > aside { position: sticky; top: 5.5rem; }
}

.skip-link {
    position: absolute; left: -9999px; z-index: 999;
    padding: .75rem 1.25rem;
    background: var(--sea); color: #fff;
    border-radius: 0 0 var(--r) 0; font-weight: 600;
}
.skip-link:focus { left: 0; top: 0; }

/* --------------------------------------------------------------------------
   5. Typography
   -------------------------------------------------------------------------- */
h1, h2, h3, h4 {
    font-family: var(--font-display);
    /* WEIGHT IS THE DISPLAY TREATMENT NOW.
     *
     * With a serif, contrast against body copy came from the FAMILY, so 500
     * was plenty and anything heavier looked shouty. A geometric sans has no
     * such contrast to trade on: at 500 a headline is just large body text,
     * which is exactly how the first pass of this redesign looked. 800 is what
     * makes it read as display type.
     *
     * `font-optical-sizing` is gone with Fraunces — it was a variable-serif
     * feature and does nothing here. */
    font-weight: 800;
    line-height: 1.05;
    /* Heavier and larger both want tighter tracking; -.03em was tuned for a
       serif at 500 and reads slack on this. */
    letter-spacing: -.035em;
    text-wrap: balance;
    color: var(--ink);
}

h1 { font-size: var(--t-2xl); }
h2 { font-size: var(--t-xl); }
h3 { font-size: var(--t-lg); letter-spacing: -.02em; }
h4 { font-size: var(--t-md); letter-spacing: -.01em; }

/* A heading with a line of deck text directly under it.
   The deck used to be pulled up with margin-top:-.4rem, which collided with
   the heading's descenders — display type is set at line-height 1.04, so the
   tails of g, y and p already hang below the line box and there is nothing
   left to borrow. "How big does it get?" has three of them. So the heading
   gets enough leading to contain its own glyphs, and the deck sits below it
   with a real (small) gap instead of a negative one. */
.block-title {
    font-size: var(--t-lg);
    font-weight: 800;
    line-height: 1.2;
    letter-spacing: -.025em;
}
.block-deck { margin-top: .3rem; }

/* THE HERO HEADLINE. 400 was a Fraunces setting — a light weight at a huge
   size is an editorial move and it needs a serif's stroke contrast to survive
   it. On a sans it reads as thin, wide and unfinished. */
.display {
    font-size: var(--t-3xl);
    font-weight: 800;
    letter-spacing: -.045em;
    line-height: .96;
}

/* A numbered eyebrow — the small "01 / Species" marker that gives a long
   page a sense of chapters. */
/* A chart-style double rule: one heavy line, one hairline below it. This is
   how a chart separates a title block from its content, and it is a much
   more considered mark than a single border. */
.eyebrow {
    display: flex;
    align-items: baseline;
    gap: .8rem;
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-mute);
    padding-bottom: .8rem;
    margin-bottom: 1.6rem;
    border-bottom: 1px solid var(--ink);
    box-shadow: 0 3px 0 -2px var(--line-strong);
}
.eyebrow em {
    font-family: var(--font-display);
    font-style: normal;
    font-size: var(--t-md);
    font-weight: 600;
    letter-spacing: -.02em;
    color: var(--hue, var(--ink));
    font-variant-numeric: tabular-nums;
}
.eyebrow .spacer { margin-left: auto; letter-spacing: .1em; font-weight: 500; }

/* Each numbered section owns a hue, walked in order down the page. On the
   monochrome themes these all resolve to shades of one accent, so the same
   markup reads as restrained there and as a spectrum here. */
.section:nth-of-type(1)  { --hue: var(--h1); --hue-wash: var(--h1-wash); }
.section:nth-of-type(2)  { --hue: var(--h2); --hue-wash: var(--h2-wash); }
.section:nth-of-type(3)  { --hue: var(--h3); --hue-wash: var(--h3-wash); }
.section:nth-of-type(4)  { --hue: var(--h4); --hue-wash: var(--h4-wash); }
.section:nth-of-type(5)  { --hue: var(--h5); --hue-wash: var(--h5-wash); }
.section:nth-of-type(6)  { --hue: var(--h6); --hue-wash: var(--h6-wash); }
.section:nth-of-type(7)  { --hue: var(--h7); --hue-wash: var(--h7-wash); }
.section:nth-of-type(8)  { --hue: var(--h2); --hue-wash: var(--h2-wash); }
.section:nth-of-type(9)  { --hue: var(--h4); --hue-wash: var(--h4-wash); }

/* The rule under an eyebrow picks up its section's hue */
.section .eyebrow { border-bottom-color: var(--hue, var(--ink)); }
.section .eyebrow::before {
    content: "";
    position: absolute;
    left: 0;
    bottom: -1px;
    width: 3.5rem;
    height: 3px;
    background: var(--hue, var(--ink));
    border-radius: 2px;
}
.section .eyebrow { position: relative; }

/* Oversized figures used as design elements rather than data points */
.figure-xl {
    font-family: var(--font-display);
    font-size: clamp(2.6rem, 1.4rem + 4.4vw, 5rem);
    font-weight: 600;
    line-height: .85;
    letter-spacing: -.05em;
    font-variant-numeric: tabular-nums;
}

.lead {
    font-size: var(--t-md);
    color: var(--ink-soft);
    max-width: 60ch;
    text-wrap: pretty;
    font-weight: 300;
}

.kicker {
    font-size: var(--t-xs);
    font-weight: 700;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--sea);
    display: flex;
    align-items: center;
    gap: .7rem;
}
.kicker::before {
    content: "";
    width: 30px; height: 1px;
    background: linear-gradient(90deg, transparent, currentColor);
    flex-shrink: 0;
}

.muted { color: var(--ink-mute); }
.soft  { color: var(--ink-soft); }
.faint { color: var(--ink-faint); }
.small { font-size: var(--t-sm); }
.tiny  { font-size: var(--t-xs); }

.sci {
    font-family: var(--font-display);
    font-style: italic;
    font-weight: 400;
    color: var(--ink-mute);
}

.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }

.section-head { margin-bottom: clamp(1.5rem, 3vw, 2.75rem); }
.section-head h1, .section-head h2 { margin-bottom: .6rem; }

/* --------------------------------------------------------------------------
   6. Header
   -------------------------------------------------------------------------- */
/* Chart border: a heavy rule with a hairline inside it, the way the neat
   line frames a chart sheet. */
.masthead {
    position: sticky;
    top: 0;
    z-index: 100;
    backdrop-filter: blur(18px) saturate(150%);
    -webkit-backdrop-filter: blur(18px) saturate(150%);
    /* color-mix keeps the bar translucent in whatever the theme's paper is,
       rather than pinning it to one hard-coded tint. */
    background: color-mix(in srgb, var(--paper) 88%, transparent);
    border-bottom: 1px solid var(--line-strong);
}

.masthead-inner {
    display: flex;
    align-items: center;
    gap: 1rem;
    min-height: 68px;
    width: min(100% - 2 * var(--pad), var(--shell));
    margin-inline: auto;
}

/* THE BRAND COLOURS, both lifted from the mahi's own row in the species
   colour table: the blue off its back and the green off its flank. Fish in
   blue, Rankings in green — the two halves of the name in the two halves of
   the fish standing next to them.

   Each has a light and a dark variant, because the catalogue values are
   swatches for a drawing, not ink for type. The flank green #6fd06a scores
   1.7:1 on the site's cream — perfectly correct and completely unreadable.
   The -ink versions keep the hue and saturation and drop the lightness until
   they clear AA: 4.8:1 for the blue, 5.1:1 for the green. The Deep theme
   inverts it and uses the bright originals, where they hit 8.6:1 and 7.3:1.

   Contrast was measured, not eyeballed. A brand colour you cannot read is
   not a brand colour. */
:root {
    --mahi-blue:      #4fb8e0;
    --mahi-blue-ink:  #0a6fa8;
    --mahi-green:     #6fd06a;
    --mahi-green-ink: #297425;

    /* THE LOGO'S OWN COLOURS — the wordmark now matches the mark beside it.

       These are lifted straight out of assets/img/logo.svg: the two-stop deep
       water the losing fins are cut from, and three stops off the leader's
       five-stop metallic ramp. The mahi values above still drive the fish
       illustrations; they simply no longer drive the brand, because the brand
       is no longer a mahi.

       -ink is the darkened cut for light backgrounds, exactly as the mahi pair
       needed: raw #d9a021 gold reads at about 2:1 on the site's paper, which
       is unreadable however correct it is to the metal. #8a5c12 is the same
       hue at roughly 7:1. */
    --logo-water-deep: #06496d;
    --logo-water:      #2f8fbe;
    --logo-gold-ink:   #b8801a;   /* the brightest gold that still clears 3:1 on paper */
    --logo-gold:       #d9a021;
    --logo-gold-lit:   #f0c945;
}

.brand {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-shrink: 0;
}

/* Fraunces at its display optical size.
   The face was already loaded with the full 9–144 opsz axis and the wordmark
   was using the default text cut, which is drawn to stay sturdy at 14px —
   low stroke contrast, open spacing, deliberately unremarkable. That is the
   wrong end of the axis for two words set at 22px whose whole job is to be
   looked at. opsz 144 is the same typeface with the contrast and the tighter
   fit it was designed to have at display sizes, so this costs no extra font
   request. */
/* The wordmark's type.

   This block was destroyed by an over-greedy edit while the sheen was being
   removed: the regex meant for the gradient rule swallowed this one too, and
   the wordmark silently fell back to the body font — Outfit 15px regular
   instead of Fraunces 1.42rem at wght 700. It rendered as ordinary running
   text and looked, at a glance, merely clean. */
.brand-word {
    /* --font-mark, NOT --font-display. When the display token went from a
       serif to a geometric sans this rule followed it and quietly restyled the
       logo — and the opsz axis below, which is a Fraunces axis, was left
       pointing at a family that has no such thing. A wordmark is identity, not
       interface; it does not move when the UI does. */
    font-family: var(--font-mark);
    font-size: 1.42rem;
    /* wght is named alongside opsz on purpose. Engines differ on whether an
       axis left out of font-variation-settings keeps its font-weight value or
       snaps back to the axis default, and a wordmark that renders at 700 in
       one browser and 400 in another is not a wordmark. font-weight stays as
       the fallback for the static cut. */
    font-variation-settings: 'opsz' 144, 'wght' 700;
    font-weight: 700;
    letter-spacing: -.012em;
    white-space: nowrap;
}

/* SOLID INK, not a gradient.

   The gradient was doing two jobs badly: it made the name itself decorative,
   and it left nothing for an accent to sit against. The name now reads as
   type — near-black, which is what a wordmark usually wants — and the ONE
   coloured thing in the lockup is the .com, which is where the eye should land
   second. A single accent against neutral type is worth more than a ramp
   across everything. */
.brand-word {
    color: var(--ink);
    -webkit-text-fill-color: currentColor;
    background: none;
}

/* The domain, in the logo's gold.

   Smaller and lighter than the name — it is a suffix, not a third word — and
   set in the body face rather than Fraunces, so it reads as a tag beside the
   wordmark instead of part of it. */
.brand-word-tld {
    font-family: var(--font-body);
    font-size: .5em;
    font-weight: 600;
    font-variation-settings: normal;
    letter-spacing: 0;
    color: var(--logo-gold-ink);
    -webkit-text-fill-color: var(--logo-gold-ink);
    vertical-align: baseline;
    margin-left: .08em;
}

/* Both halves inherit the one gradient, so the colour transition falls where
   the letters are rather than at the join between the two spans. */
.brand-word-accent { color: inherit; }

:root[data-theme="harbor"] .brand-word { color: var(--ink); }

/* The ink gold goes muddy on deep water, so the accent takes the lit cut —
   the same swap the mark makes. */
:root[data-theme="harbor"] .brand-word-tld {
    color: var(--logo-gold-lit);
    -webkit-text-fill-color: var(--logo-gold-lit);
}

/* Last line of defence: if the browser cannot clip a background to text, the
   name must still be legible. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
    .brand-word {
        background: none;
        color: var(--mahi-blue-ink);
        -webkit-text-fill-color: var(--mahi-blue-ink);
        animation: none;
    }
    .brand-word-accent { color: var(--mahi-green-ink); -webkit-text-fill-color: var(--mahi-green-ink); }
    :root[data-theme="harbor"] .brand-word { color: var(--mahi-blue); -webkit-text-fill-color: var(--mahi-blue); }
}
/* The mark is a FishArt drawing, so it arrives at the renderer's own aspect
   ratio (roughly 2:1) rather than as a square glyph. Height is fixed and
   width follows, which is the only way a logo that is literally the species
   illustration stays undistorted if the renderer's frame ever changes. */
.brand-mark { display: flex; align-items: center; flex-shrink: 0; }
.brand svg { height: 34px; width: auto; flex-shrink: 0; }
.brand:hover svg { animation: swim 1.2s var(--ease); }

/* The split is on the LAST space, so "Fish" takes the blue and "Rankings" the
   green. That is a guess about grammar, and it is only safe because the brand
   name is not translated — app.name is "Fish Rankings" in both language files
   by design, the way brand names normally are. A one-word name falls through
   to the blue with no accent span at all rather than breaking. */

@keyframes swim {
    0%, 100% { transform: translateX(0) rotate(0); }
    30%      { transform: translateX(4px) rotate(-5deg); }
    62%      { transform: translateX(-2px) rotate(3deg); }
}

/* ---- Site search --------------------------------------------------------
   Sits between the brand and the nav on wide screens and drops to full width
   inside the mobile drawer. `margin-left:auto` here rather than on .nav so
   the search takes the slack and the links stay tight to the right edge. */
.masthead-search {
    position: relative;
    display: flex;
    align-items: center;
    margin-left: auto;
    max-width: 260px;
    flex: 1 1 160px;
}
.masthead-search svg {
    position: absolute;
    left: .6rem;
    width: 15px; height: 15px;
    color: var(--ink-faint);
    pointer-events: none;
}
.masthead-search .input {
    padding-left: 2rem;
    padding-block: .42rem;
    font-size: var(--t-sm);
    border-radius: 999px;
    background: var(--surface);
    width: 100%;
}
.masthead-search .input:focus { background: var(--card); }
.masthead-search .input::-webkit-search-cancel-button { cursor: pointer; }

.nav { display: flex; align-items: center; gap: .15rem; }

.nav a, .nav button.navlink {
    position: relative;
    padding: .5rem .78rem;
    border-radius: var(--r-sm);
    font-size: var(--t-sm);
    font-weight: 500;
    color: var(--ink-soft);
    transition: color .18s var(--ease);
    white-space: nowrap;
}
.nav a:hover, .nav button.navlink:hover { color: var(--ink); }

/* Buttons in the nav keep their own text colour.
   "Log a catch" is an <a class="btn btn-primary"> sitting inside this nav, so
   .nav a (0,1,1) was beating .btn-primary's white (0,1,0) and painting soft
   grey ink over a saturated blue gradient — the one place in the header where
   the label was genuinely hard to read, and on the most important control in
   it. Two classes to win the specificity contest cleanly, and :hover restated
   because .nav a:hover was darkening it further on the way to being clicked.
   Scoped to the filled variants: .btn-ghost is supposed to inherit ink. */
.nav .btn-primary,
.nav .btn-primary:hover,
.nav .btn-gold,
.nav .btn-gold:hover { color: #fff; }

.nav a[aria-current="page"] { color: var(--ink); font-weight: 600; }
.nav a[aria-current="page"]::after {
    content: "";
    position: absolute;
    left: .78rem; right: .78rem; bottom: .18rem;
    height: 2px; border-radius: 2px;
    background: var(--sea);
}

.nav-toggle { display: none; padding: .5rem; border-radius: var(--r-sm); color: var(--ink-soft); margin-left: auto; }

@media (max-width: 920px) {
    .nav-toggle { display: block; }
    .nav {
        position: fixed;
        inset: 68px 0 auto 0;
        flex-direction: column;
        align-items: stretch;
        gap: .2rem;
        padding: 1rem var(--pad) 1.5rem;
        background: var(--paper);
        border-bottom: 1px solid var(--line);
        box-shadow: var(--shadow);
        transform: translateY(-120%);
        transition: transform .32s var(--ease);
        max-height: calc(100vh - 68px);
        overflow-y: auto;
    }
    .nav[data-open="true"] { transform: translateY(0); }
    .nav a, .nav button.navlink { padding: .8rem 1rem; font-size: var(--t-md); }
    .nav a[aria-current="page"]::after { display: none; }

    /* ---- The Guide group flattens back into a plain list ----------------
       Collapsing five destinations behind a dropdown buys horizontal space,
       and in the drawer horizontal space is not what is scarce — the drawer is
       a tall scrolling column with room to spare. Hiding five links behind an
       extra tap here would be cost with no benefit.

       So: the trigger disappears, the menu stops being a floating overlay and
       becomes part of the stack. Same markup, no duplicate list to keep in
       step with the desktop one, and it degrades to exactly the flat list that
       was there before. */
    .nav-group { display: contents; }
    .nav-group-trigger { display: none; }
    .nav-group-menu {
        display: block;
        position: static;
        min-width: 0;
        padding: 0;
        border: 0;
        border-radius: 0;
        background: none;
        box-shadow: none;
    }
    .nav-group-menu a { padding: .8rem 1rem; font-size: var(--t-md); }

    /* ---- The account menu flattens too, for a concrete reason ------------
       It is a popover anchored to an item near the bottom of a scrolling
       column, and it just got taller: the three preference groups added around
       170px to it. Measured against a 700px-tall phone, the trigger sits at
       roughly y=430 in the drawer and the menu now wants 320px — so it opened
       past the bottom edge and had to be scrolled to inside a drawer the user
       was already scrolling. An overlay inside a scroll container is the wrong
       shape here.

       The trigger stays visible, unlike the Guide group's: it shows whose
       account this is, which makes it a useful heading for the block. */
    .account-switcher { width: 100%; }
    .account-menu {
        display: block;
        position: static;
        min-width: 0;
        margin-top: .2rem;
        padding: .3rem 0 0;
        border: 0;
        border-radius: 0;
        background: none;
        box-shadow: none;
    }
    .account-menu > a, .account-menu form > button { padding: .8rem 1rem; font-size: var(--t-md); }
    .pref-row .pref-chip { padding: .55rem .6rem; font-size: var(--t-sm); }

    /* The signed-out gear is now redundant — its only contents are the
       preferences, which are already open below it in the drawer. */
    .switcher-icon { display: none; }

    /* The search keeps its place in the bar rather than moving into the
       drawer: on a phone, search is the thing you most want one tap away. */
    .masthead-search { max-width: none; flex: 1 1 auto; margin-inline: .5rem 0; }
}

@media (max-width: 520px) {
    /* Below this the brand, a search field and a menu button cannot all fit
       without the field becoming a stub. The brand mark stays, its wordmark
       goes, and search gets the room. */
    .brand span { display: none; }
    .masthead-search { margin-left: .25rem; }
}

.switcher { position: relative; }
.switcher > button {
    display: flex; align-items: center; gap: .4rem;
    padding: .42rem .68rem;
    border-radius: var(--r-sm);
    font-size: var(--t-sm);
    color: var(--ink-soft);
    border: 1px solid var(--line-strong);
    background: var(--card);
    transition: border-color .18s var(--ease), color .18s var(--ease);
}
.switcher > button:hover { border-color: var(--sea); color: var(--ink); }

.switcher-menu {
    position: absolute;
    right: 0;
    top: calc(100% + .4rem);
    min-width: 172px;
    padding: .3rem;
    border-radius: var(--r);
    background: var(--card);
    border: 1px solid var(--line);
    box-shadow: var(--shadow);
    display: none;
    z-index: 50;
}
.switcher[data-open="true"] .switcher-menu { display: block; }
.switcher-menu a, .switcher-menu button {
    display: flex; align-items: center; gap: .6rem;
    width: 100%;
    padding: .55rem .7rem;
    border-radius: var(--r-sm);
    font-size: var(--t-sm);
    color: var(--ink-soft);
    text-align: left;
}
.switcher-menu a:hover, .switcher-menu button:hover { background: var(--sea-wash); color: var(--ink); }
.switcher-menu [aria-current="true"] { color: var(--sea); font-weight: 600; }
/* Nav links live inside menus now, and they mark themselves with
   aria-current="page" rather than "true". Same treatment. */
.switcher-menu a[aria-current="page"] { color: var(--sea); font-weight: 600; }
/* .nav a[aria-current="page"]::after draws an underline bar positioned against
   the nearest positioned ancestor. Inside a menu that ancestor is .switcher,
   so the bar landed on the trigger instead of the link — a stray blue dash
   under the closed dropdown. Menus mark the current item by weight and colour;
   they do not need the bar. */
.switcher-menu a[aria-current="page"]::after { display: none; }

/* An icon-only trigger wants to be square, not a wide pill with a glyph
   rattling around in it. */
.switcher-icon { padding: .42rem; }
.switcher-icon svg { width: 18px; height: 18px; display: block; }

/* Long display names must not push the bar wide again — the whole point of
   this exercise. Truncate instead. */
.switcher-name {
    max-width: 12ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Nav groups — several destinations behind one item

   The reference half of the site (species, seasonality, map, charters, tackle)
   sits behind one trigger so the bar fits its own shell. The trigger has to do
   something a plain link does not: report that one of the pages inside it is
   the current page, even while closed. A group that goes anonymous the moment
   you enter it is worse than the flat list it replaced.
   -------------------------------------------------------------------------- */
.nav-group-trigger {
    /* Reads as a nav link, not as a form control. The bordered .switcher pill
       is right for a settings widget and wrong for a destination — five
       destinations in a box, sitting between two bare text links, looks like a
       different kind of thing than it is. */
    border-color: transparent !important;
    background: transparent !important;
    font-weight: 500;
    padding: .5rem .6rem .5rem .78rem;
}
.nav-group-trigger:hover { color: var(--ink); }

.nav-chevron {
    width: 14px; height: 14px;
    opacity: .55;
    transition: transform .18s var(--ease);
}
.switcher[data-open="true"] .nav-chevron { transform: rotate(180deg); }

/* Current-page state on the trigger, mirroring .nav a[aria-current="page"]
   including the underline — so "you are in here somewhere" reads exactly like
   "you are here" does on the flat links either side of it. */
.nav-group-trigger[aria-current="page"] { color: var(--ink); font-weight: 600; }
.nav-group-trigger[aria-current="page"]::after {
    content: "";
    position: absolute;
    left: .78rem; right: .6rem; bottom: .18rem;
    height: 2px; border-radius: 2px;
    background: var(--sea);
}
.nav-group { position: relative; }

/* The menu opens under the trigger rather than flush right, because this one
   hangs off an item in the middle of the bar, not off the end of it. */
.nav-group-menu { right: auto; left: 0; min-width: 190px; }

/* --------------------------------------------------------------------------
   Preferences panel (language / theme / units) inside a menu

   Three settings that used to be three permanent dropdowns in the masthead.
   Rendered as labelled rows of chips rather than nested submenus: every option
   visible, one click each, and no menu-inside-a-menu for the switcher script
   to fight with.
   -------------------------------------------------------------------------- */
.account-menu { min-width: 244px; }

.prefs {
    margin-top: .3rem;
    padding-top: .45rem;
    border-top: 1px solid var(--line);
}

.pref-group + .pref-group { margin-top: .5rem; }

.pref-label {
    margin: 0 0 .3rem;
    padding-inline: .7rem;
    font-size: .68rem;
    font-weight: 600;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.pref-row { display: flex; gap: .3rem; padding-inline: .4rem; }
/* The unit chips are each wrapped in a form; the form must not become the
   flex item, or the chips lose their even split of the row. */
.pref-row form { display: contents; }

/* Beats .switcher-menu a / .switcher-menu button (0,1,1) with two classes, so
   the chips sit side by side instead of each claiming a full-width row. */
.pref-row .pref-chip {
    display: flex;
    align-items: center;
    gap: .35rem;
    flex: 1 1 0;
    width: auto;
    min-width: 0;
    padding: .4rem .5rem;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    font-size: var(--t-xs);
    color: var(--ink-soft);
    background: transparent;
    white-space: nowrap;
    overflow: hidden;
}
.pref-row .pref-chip:hover { border-color: var(--sea); color: var(--ink); background: transparent; }
.pref-row .pref-chip[aria-current="true"] {
    border-color: var(--sea);
    background: var(--sea-wash);
    color: var(--sea);
    font-weight: 600;
}
.pref-chip span:last-child { overflow: hidden; text-overflow: ellipsis; }

/* The unit chips are the one row where the short form carries the meaning, so
   it gets the emphasis and the spelled-out name plays support. */
.pref-unit {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    flex: none;
}

/* ---- Theme swatch -------------------------------------------------------
   .theme-menu and .theme-option lived here, styling the standalone theme
   dropdown that used to sit in the masthead. That dropdown is gone — theme is
   one of the three chips in the preferences panel now — so the rules went with
   it rather than being left behind as plausible-looking dead code.

   The swatch itself stays, because the chips still use it: three stops for
   paper, water and accent, which is enough to judge a palette without
   applying it. */
.theme-swatch {
    display: inline-grid;
    grid-template-columns: repeat(3, 1fr);
    width: 26px;
    height: 14px;
    border-radius: 3px;
    overflow: hidden;
    border: 1px solid var(--line-strong);
    flex-shrink: 0;
}
.theme-swatch i { display: block; }

@media (max-width: 920px) {
    .switcher-menu { position: static; display: block; border: none; background: none; box-shadow: none; padding: 0; }
    .switcher > button { display: none; }
}

/* --------------------------------------------------------------------------
   7. Buttons
   -------------------------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    padding: .72rem 1.35rem;
    border-radius: 999px;
    font-size: var(--t-sm);
    font-weight: 600;
    border: 1px solid transparent;
    transition: transform .2s var(--ease), box-shadow .26s var(--ease),
                background .2s var(--ease), border-color .2s var(--ease);
    white-space: nowrap;
}
.btn:active { transform: translateY(1px) scale(.99); }

.btn-primary {
    background: linear-gradient(165deg, var(--sea-bright), var(--sea-deep));
    color: #fff;
    box-shadow: 0 2px 6px rgba(13, 95, 92, .2), 0 8px 20px rgba(13, 95, 92, .16);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(13, 95, 92, .22), 0 14px 30px rgba(13, 95, 92, .22); }

.btn-ghost { border-color: var(--line-strong); color: var(--ink); background: var(--card); }
.btn-ghost:hover { border-color: var(--sea); color: var(--sea); box-shadow: var(--shadow-sm); }

.btn-gold {
    background: linear-gradient(165deg, var(--gold-bright), var(--gold));
    color: #fff;
    box-shadow: 0 2px 6px rgba(156, 47, 102, .2), 0 8px 20px rgba(156, 47, 102, .16);
}
.btn-gold:hover { transform: translateY(-2px); }

.btn-sm { padding: .44rem .9rem; font-size: var(--t-xs); }
.btn-lg { padding: .92rem 2rem; font-size: var(--t-base); }
.btn-block { width: 100%; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; pointer-events: none; }

.btn-danger { border-color: rgba(176, 74, 74, .35); color: var(--clay); background: rgba(176, 74, 74, .05); }
.btn-danger:hover { background: rgba(176, 74, 74, .11); }

/* --------------------------------------------------------------------------
   8. Panels
   -------------------------------------------------------------------------- */
/* Opaque by design. Everything the reader has to read sits on a solid
   surface; the reef is allowed to show only in the gaps between them. */
.panel {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    padding: clamp(1.15rem, 2.5vw, 1.85rem);
    box-shadow: var(--shadow-sm);
}
.panel-flush { padding: 0; overflow: hidden; }
.panel-solid { background: var(--paper-warm); box-shadow: none; }

/* The footer sits directly on top of the reef, so it needs to be opaque or
   the coral runs straight through the link columns. */
.footer { position: relative; z-index: 1; }

/* --------------------------------------------------------------------------
   9. Species imagery
   -------------------------------------------------------------------------- */
.species-media {
    position: relative;
    aspect-ratio: var(--ratio, 16 / 10);
    overflow: hidden;
    background:
        linear-gradient(170deg, var(--surface), transparent 60%),
        color-mix(in srgb, var(--tint, #8fb2ca) 24%, var(--w-050));
    display: grid;
    /* ONE EXPLICIT TRACK, so the row has a DEFINITE height. The tile's own
       height is definite (aspect-ratio), but an implicit `auto` row is sized by
       its content, and a percentage max-height measured against an indefinite
       box resolves to `none` — so nothing inside a fixed tile can be told how
       tall the frame actually is. The plate opts back out of this; see there. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    place-items: center;
}

@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .species-media { background: var(--w-050); }
}

.species-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform .8s var(--ease), filter .5s var(--ease);
    /* Photographs from a hundred sources have wildly different grading. A
       light uniform treatment pulls them into one visual family. */
    filter: saturate(1.02) contrast(1.02);
}

/* ── THE PLATE ────────────────────────────────────────────────────────────
   An illustration is not a photograph and should stop pretending to be one.

   This tile used to paint the species' accent colour behind the drawing at
   24% and let it run 92% of the width. A saturated fish, hero-sized, on a
   pastel panel is a children's book plate — and no amount of added realism
   in the drawing changes that, because the register is set by the GROUND,
   the SCALE and the SATURATION, not by detail. Four passes of extra fidelity
   proved it.

   So the illustration gets the opposite treatment: warm paper, a hairline
   rule, generous margins, and the fish at about two thirds of the width with
   its species named underneath in italic. That is the natural-history plate —
   Peterson, the IGFA yearbook — and it is the visual language of a reference
   work, which is what this product actually is.

   A PHOTOGRAPH still gets the full-bleed tile. It has earned it: a real fish
   in real hands is the most premium image on the site, so it fills its frame
   while the drawing sits politely inside a margin. */
.species-media-plate {
    background: var(--plate-paper);
    border: 1px solid var(--plate-rule);
    display: grid;
    place-items: center;
    padding: clamp(1.1rem, 4vw, 2.4rem);

    /* ITS OWN RATIO, overriding whatever the caller asked for. The 4/3 the
       species page passes is chosen for a PHOTOGRAPH; a fish is a wide, flat
       subject and at 4/3 a marlin floats in a field of empty paper. Plates are
       cut to their specimen — and for the deep-bodied ones that now means
       taller, because of the two lines below. */
    aspect-ratio: 16 / 9;

    /* NO CLIPPING, AND THAT IS THE WHOLE MECHANISM.
     *
     * The tile above hides its overflow so a photograph can be cropped to the
     * frame by object-fit. A plate has nothing to crop: the drawing sits inside
     * the padding. But overflow:hidden also makes the box a scroll container,
     * and a scroll container's automatic minimum size is zero — which is
     * precisely why a fish taller than 16/9 was being shaved off instead of
     * pushing the paper down. Let it be visible and `aspect-ratio` goes back to
     * meaning what it should here: the SMALLEST the plate may be, not the only
     * shape it may take. A turbot gets a taller sheet; a marlin is untouched. */
    overflow: visible;
    grid-template-rows: auto;

    /* A tighter corner than the photo tile. A large radius reads as an app
       card; a plate is closer to a printed page. */
    border-radius: 5px;
}
.species-media-plate::after {
    /* No vignette on paper — the inner shadow seats a photograph on the page
       and merely dirties a plate. */
    display: none;
}
.species-media-art {
    width: min(66%, 420px);
    display: grid;
    place-items: center;
    transition: transform .7s var(--ease);
}

/* THE HEIGHT HAS TO BE GOVERNED TOO — the width alone is not the frame.
 *
 * FishArt's viewBox height is per species: a barracuda is 420 × 210 and a
 * turbot is 420 × 808, because the box grows downward rather than scaling the
 * fish down to a fixed 2:1. Sizing the drawing by width alone therefore says
 * nothing about how tall it lands, and on a deep-bodied fish it landed a long
 * way past the bottom of its frame. The turbot lost everything below its
 * pectoral; the permit, the bluegill and the roosterfish lost belly and anal
 * fin. width:100% still drives the ordinary case, so a slim fish sits in its
 * margin exactly as before — the height rule only bites when the drawing is
 * taller than the frame. */
.species-media-art svg {
    width: 100%;
    height: auto;
}

/* ON THE PLATE the paper moves and the fish is left alone, up to a ceiling.
 * 330 px is the width cap below, so the tallest a specimen may stand is the
 * longest one may run: a turbot lands on roughly a 4/3 sheet rather than a
 * portrait one. An absolute length rather than a percentage on purpose — this
 * is the number the grid reads when it sizes the row, and a percentage would
 * be cyclic there and get ignored. */
.species-media-plate .species-media-art svg { max-height: 330px; }

/* ON A FIXED TILE nothing can move, so the fish is contained instead: the box
 * takes the frame's full height and the drawing shrinks whole to fit it. The
 * CSS min/max algorithm re-derives the width from the intrinsic ratio, which
 * is the same contain-fit Certificate::art() does in PHP, for the same reason.
 * (Only reachable when a caller asks for the bare tile — every current one
 * gets the plate — but a frame that cannot grow must not crop either.) */
.species-media:not(.species-media-plate) .species-media-art {
    align-self: stretch;
    min-height: 0;
}
.species-media:not(.species-media-plate) .species-media-art svg { max-height: 100%; }

/* SMALLER THAN FEELS RIGHT, and that is the point. At 69% of the tile the
   drawing was still being asked to carry the page; at 54% it sits in its
   margin and the paper around it does the work. A plate reads as confident
   BECAUSE it is not filling the frame — the white space is what says
   "specimen" rather than "poster". */
.species-media-plate .species-media-art { width: min(54%, 330px); }

/* The caption. A plate without one is a picture; with one it is a reference. */
.plate-caption {
    margin-top: .85rem;
    text-align: center;
    font-family: var(--font-display), Georgia, serif;
    font-style: italic;
    font-size: var(--t-sm);
    color: var(--plate-ink);
    letter-spacing: .01em;
}
.plate-caption b { font-style: normal; font-weight: 500; }

/* On paper the vignette is a soft inner shade, not a black gradient —
   it seats the photo on the page without dirtying it. */
.species-media::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    box-shadow: inset 0 0 0 1px rgba(var(--shadow-rgb), .08),
                inset 0 -30px 44px -30px rgba(var(--shadow-rgb), .24);
}
.species-media-plain::after { display: none; }

.image-credit { margin-top: .5rem; color: var(--ink-faint); line-height: 1.45; }
.image-credit a { color: var(--ink-mute); text-decoration: underline; text-underline-offset: 2px; }
.image-credit a:hover { color: var(--sea); }

@media (prefers-reduced-motion: no-preference) {
    .card:hover .species-media-art { transform: translateX(6px) scale(1.04); }
}

/* --------------------------------------------------------------------------
   10. Cards
   -------------------------------------------------------------------------- */
.card {
    position: relative;
    display: flex;
    flex-direction: column;
    border-radius: var(--r-lg);
    background: var(--card);
    border: 1px solid var(--line);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
    transition: transform .4s var(--ease), border-color .4s var(--ease), box-shadow .4s var(--ease);
    isolation: isolate;
}
.card:hover { transform: translateY(-4px); border-color: var(--line-strong); box-shadow: var(--shadow); }
.card:hover .species-media img { transform: scale(1.05); }

.card-media { position: relative; display: block; }

.card-body { padding: 1.05rem 1.15rem 1.2rem; display: flex; flex-direction: column; gap: .45rem; flex: 1; }
.card-title { font-size: var(--t-md); font-family: var(--font-display); font-weight: 600; letter-spacing: -.018em; line-height: 1.2; }
.card-title a:hover { color: var(--sea); }
.card-foot {
    margin-top: auto;
    padding-top: .8rem;
    border-top: 1px solid var(--line);
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: .5rem;
    font-size: var(--t-xs);
}

.card-overlay {
    position: absolute;
    left: .85rem; right: .85rem; bottom: .8rem;
    z-index: 2;
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: .5rem;
    pointer-events: none;
}

.rank-badge {
    display: grid;
    place-items: center;
    min-width: 2rem;
    height: 2rem;
    padding-inline: .42rem;
    border-radius: 999px;
    font-family: var(--font-display);
    font-size: var(--t-sm);
    font-weight: 700;
    background: var(--paper-warm);
    color: var(--ink-soft);
    border: 1px solid var(--line);
}
/* THE PODIUM, matching the app's.
   Gold, silver, bronze — three metals, which is what a podium is. The old set
   was gold, then SEA, then sand: the second place was the site's teal accent,
   so the top three read as "winner, a link, and a beige one" rather than as a
   medal stack. Dark type on all three, because white on this gold is 1.9:1,
   and a lifted shadow so the disc sits on the row the way the app's does. */
.rank-badge[data-rank="1"],
.rank-badge[data-rank="2"],
.rank-badge[data-rank="3"] {
    color: var(--ink);
    border-color: transparent;
    box-shadow: 0 1px 2px rgba(11,17,24,.22), 0 3px 8px rgba(11,17,24,.14);
}
.rank-badge[data-rank="1"] { background: linear-gradient(165deg, #F0C945, #D9A021); }
.rank-badge[data-rank="2"] { background: linear-gradient(165deg, #DCE5EB, #B6C4CE); }
.rank-badge[data-rank="3"] { background: linear-gradient(165deg, #E3BE96, #C1936A); }

[data-theme="harbor"] .rank-badge[data-rank="2"] { background: linear-gradient(165deg, #B9C7D2, #8FA1AE); }
[data-theme="harbor"] .rank-badge[data-rank="3"] { background: linear-gradient(165deg, #C79A6E, #A2744C); }

/* ==========================================================================
   10b. THE INDEX TABLE
   --------------------------------------------------------------------------
   The primary layout of the site. A card grid gives every row identical
   visual weight, which is wrong for a reference work — a 700 kg marlin and a
   400 g perch are not equally significant, and a grid cannot say so. A table
   can: it ranks, it aligns figures for comparison, and it lets one column
   carry a shared-scale silhouette so the size relationship is legible by
   scanning rather than by arithmetic.

   It is also simply what a field guide looks like.
   ========================================================================== */
.index {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: var(--t-sm);
}

/* Framed the way a chart tabulation is: heavy rules top and bottom.
   Fully opaque — the reef behind the page must never show through a table of
   figures, which is the difference between atmosphere and noise. */
.index-wrap {
    overflow-x: auto;
    border-top: 1.5px solid var(--ink);
    border-bottom: 1.5px solid var(--ink);
    background: var(--card);
    box-shadow: 0 1px 24px rgba(var(--shadow-rgb), .06);
}

/* Alternating tint, very faint — chart tables band their rows so the eye
   can track across a wide row without losing its line. */
.index tbody tr:nth-child(even) { background: var(--surface); }

.index thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    text-align: left;
    padding: .85rem 1rem;
    font-size: var(--t-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .12em;
    color: var(--ink-mute);
    background: var(--paper);
    border-bottom: 1px solid var(--ink);
    white-space: nowrap;
    vertical-align: bottom;
}
.index thead th a { color: inherit; display: inline-flex; align-items: center; gap: .3rem; }
.index thead th a:hover { color: var(--sea); }
.index thead th[aria-sort] { color: var(--ink); }
.index thead th[aria-sort]::after {
    content: "↓";
    font-size: .9em;
    margin-left: .25rem;
}
.index thead th[aria-sort="ascending"]::after { content: "↑"; }

.index tbody td {
    padding: .7rem 1rem;
    border-bottom: 1px solid var(--line);
    vertical-align: middle;
}
.index tbody tr:last-child td { border-bottom: none; }
.index tbody tr { transition: background .16s var(--ease); }
.index tbody tr:hover { background: var(--sea-wash); }

.index .num-cell { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Rank column — set in display face so the numerals read as typography */
.index .rank {
    width: 1%;
    font-family: var(--font-display);
    font-size: var(--t-md);
    font-weight: 600;
    color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
    letter-spacing: -.02em;
    padding-right: .25rem;
}
.index tbody tr:hover .rank { color: var(--sea); }
.index tr[data-rank="1"] .rank,
.index tr[data-rank="2"] .rank,
.index tr[data-rank="3"] .rank { color: var(--gold); }

/* Thumbnail column */
.index .thumb { width: 1%; padding-right: 0; }
.index-thumb {
    width: 84px;
    aspect-ratio: 16 / 11;
    border-radius: 6px;
    overflow: hidden;
    background: var(--w-050);
    display: grid;
    place-items: center;
    border: 1px solid var(--line);
}
.index-thumb img { width: 100%; height: 100%; object-fit: cover; }
/* Fish art has a variable aspect ratio now, so constrain both axes and let
   the browser pick — otherwise a turbot overflows its thumbnail. */
/* Backed off from 94/92 to leave the swim somewhere to go. The thumbnail
   clips its overflow, and at the old size a fin tip crossed the edge at the
   top of every cycle — the fish appeared to be nibbling the border. */
.index-thumb svg { max-width: 90%; max-height: 84%; width: auto; height: auto; }

@media (min-width: 900px) {
    .index-thumb { width: 104px; }
}

/* ---- Tackle photographs -------------------------------------------------
   DELIBERATELY NOT .index-thumb, which is three rules up and is the element
   the fish swim is bound to. Borrowing it would give a reel the drift and the
   pitch of a mackerel, which is funny once.

   And there is no fallback here on purpose. The species row falls back to a
   drawing because FishArt can draw any fish from its traits; there is no
   equivalent for tackle — nothing can generate a photograph of a particular
   reel — and the category glyph the app used to show in its place looked like
   a picture of the item while telling you only what the name already said. So
   a photograph appears when somebody took one, and otherwise nothing does.
   ------------------------------------------------------------------------ */
.gear-thumb {
    width: 40px;
    height: 40px;
    border-radius: 6px;
    object-fit: cover;
    vertical-align: middle;
    margin-right: .55rem;
    border: 1px solid var(--line);
    background: var(--surface);
}

.gear-photo {
    display: block;
    width: 100%;
    max-width: 420px;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: var(--r);
    border: 1px solid var(--line);
    background: var(--surface);
}

/* Name column */
.index .title {
    font-family: var(--font-display);
    font-size: var(--t-md);
    font-weight: 600;
    letter-spacing: -.02em;
    line-height: 1.2;
    display: block;
}
.index .title:hover { color: var(--sea); }
.index .subtitle { font-size: var(--t-xs); }

/* The size column. Every silhouette in the table is drawn from one shared
   millimetres-per-pixel ratio, so scanning the column gives you the size
   relationship directly — this is the reason the table exists. */
.index .size-cell { width: 34%; min-width: 190px; }
.index-size {
    position: relative;
    display: flex;
    align-items: center;
    min-height: 34px;
}
.index-size-rule {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background: repeating-linear-gradient(90deg,
        transparent 0 calc(var(--tick, 40px) - 1px),
        var(--line-strong) calc(var(--tick, 40px) - 1px),
        var(--line-strong) var(--tick, 40px));
}
.index-size-fish {
    position: relative;
    z-index: 1;
    display: block;
    transform-origin: left center;
    transition: transform .3s var(--ease);
}
/* The span owns the width — that is what carries the shared scale. The SVG
   fills it and takes whatever height its own aspect ratio needs, which is how
   a deep-bodied turbot and a slim barracuda can sit in the same column at the
   same length-per-pixel. */
.index-size-fish svg { width: 100%; height: auto; display: block; }
.index tbody tr:hover .index-size-fish { transform: scale(1.03); }

/* .index-bar lived here — a comparison bar scaled against the page leader, used
   by the anglers directory's "Relative" column. Both are gone: the bar sat
   immediately left of the exact weight it was scaling, so it spent a column
   restating the number next to it, less precisely, in a table already sorted by
   that number. Deleted rather than left in place, because unused CSS with a
   confident comment explaining a column that no longer exists is how a
   stylesheet starts lying to the next person reading it. */

/* Record weights get the display face — they are the headline figure */
.index .record {
    font-family: var(--font-display);
    font-size: var(--t-md);
    font-weight: 600;
    letter-spacing: -.02em;
    color: var(--gold);
    font-variant-numeric: tabular-nums;
}

/* Columns that vanish first on narrow screens, least important first */
@media (max-width: 1100px) { .index .opt-3 { display: none; } }
@media (max-width: 900px)  { .index .opt-2 { display: none; } }
@media (max-width: 700px)  {
    .index .opt-1 { display: none; }
    .index thead th, .index tbody td { padding: .6rem .5rem; }
    .index-thumb { width: 64px; }
    .index .size-cell { min-width: 110px; }
}

.index-empty { padding: clamp(3rem, 8vw, 5rem) 1.5rem; text-align: center; color: var(--ink-mute); }

/* Caption under a table, in the manner of a printed plate */
.index-note {
    margin-top: .9rem;
    font-size: var(--t-xs);
    color: var(--ink-mute);
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

/* --------------------------------------------------------------------------
   11. Chips, tiers, ratings
   -------------------------------------------------------------------------- */
.chip {
    display: inline-flex;
    align-items: center;
    gap: .32rem;
    padding: .2rem .58rem;
    border-radius: 999px;
    font-size: var(--t-xs);
    font-weight: 500;
    background: var(--surface);
    border: 1px solid var(--line);
    color: var(--ink-soft);
    white-space: nowrap;
}
.chip-glow  { color: var(--sea); border-color: var(--sea); background: var(--sea-wash); }

/* Water type gets its own hue, so a saltwater and a freshwater species are
   distinguishable in a table without reading the label. */
.chip[data-water="salt"]       { color: var(--h2); border-color: var(--h2); background: var(--h2-wash); }
.chip[data-water="brackish"]   { color: var(--h3); border-color: var(--h3); background: var(--h3-wash); }
.chip[data-water="fresh"]      { color: var(--h4); border-color: var(--h4); background: var(--h4-wash); }
.chip[data-water="anadromous"] { color: var(--h1); border-color: var(--h1); background: var(--h1-wash); }

/* Depth zone chips inherit the descent through the spectrum */
.chip[data-zone="surface"]      { color: var(--h5); border-color: var(--h5); background: var(--h5-wash); }
.chip[data-zone="reef"]         { color: var(--h6); border-color: var(--h6); background: var(--h6-wash); }
.chip[data-zone="epipelagic"]   { color: var(--h4); border-color: var(--h4); background: var(--h4-wash); }
.chip[data-zone="mesopelagic"]  { color: var(--h3); border-color: var(--h3); background: var(--h3-wash); }
.chip[data-zone="demersal"]     { color: var(--h2); border-color: var(--h2); background: var(--h2-wash); }
.chip[data-zone="bathypelagic"] { color: var(--h1); border-color: var(--h1); background: var(--h1-wash); }
.chip[data-zone="freshwater"]   { color: var(--moss); border-color: var(--moss); background: var(--h4-wash); }
.chip-gold  { color: var(--mark); border-color: var(--mark); background: var(--mark-wash); }
.chip-coral { color: var(--rust); border-color: var(--rust); background: var(--mark-wash); }
/* Chips that sit on top of a photograph need their own background */
.chip-solid { background: var(--card); border-color: var(--line-strong); color: var(--ink);
              backdrop-filter: blur(6px); box-shadow: 0 1px 4px rgba(var(--shadow-rgb), .16); }

.tier {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .18rem .52rem;
    border-radius: 6px;
    font-size: var(--t-xs);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
/* THE SCORE CHIP AND THE RECORD BAR ARE THE SAME SCALE, so they are the same
   colours. They were not: the chip ran mark → mark → rust → sea → grey while
   the bar ran its own hues, so one Trophy Score could be pink in the column
   and green in the bar of the same row. Both now come from the tokens below,
   which is also what stops them drifting apart again.

   The chip is the bar's colour as INK on a wash of itself, rather than the
   fill — a solid bar reads at 10px tall and a solid chip at 11px type does
   not, and the two only have to agree about hue. */
:root, [data-theme] {
    --tier-logged:    #5f7c90;
    --tier-notable:   #12708a;
    --tier-trophy:    #14795a;
    --tier-legendary: #b4711d;
    --tier-record:    #b8860b;
}
[data-theme="harbor"] {
    --tier-logged:    #93a9b8;
    --tier-notable:   #4fb4d6;
    --tier-trophy:    #3ec294;
    --tier-legendary: #f0b44e;
    --tier-record:    #e8b830;
}

.tier-logged {
    background: color-mix(in srgb, var(--tier-logged) 12%, transparent);
    color: var(--tier-logged);
    border: 1px solid color-mix(in srgb, var(--tier-logged) 34%, transparent);
}
.tier-notable {
    background: color-mix(in srgb, var(--tier-notable) 12%, transparent);
    color: var(--tier-notable);
    border: 1px solid color-mix(in srgb, var(--tier-notable) 34%, transparent);
}
.tier-trophy {
    background: color-mix(in srgb, var(--tier-trophy) 12%, transparent);
    color: var(--tier-trophy);
    border: 1px solid color-mix(in srgb, var(--tier-trophy) 34%, transparent);
}
.tier-legendary {
    background: color-mix(in srgb, var(--tier-legendary) 14%, transparent);
    color: var(--tier-legendary);
    border: 1px solid color-mix(in srgb, var(--tier-legendary) 40%, transparent);
}
/* THE RECORD TIER IS WASHED LIKE THE REST.
   It was filled gold with near-white ink, on the reasoning that a record is
   not a score and should look different. It did look different — which is the
   problem: the chip read as a solid gold pill next to a bar of gold ink, so
   the one tier that should be the most recognisable was the only one where
   the two columns disagreed. Same treatment as the other four, so every score
   on the site is the same colour in every place it appears. */
.tier-record {
    background: color-mix(in srgb, var(--tier-record) 16%, transparent);
    color: var(--tier-record);
    border: 1px solid color-mix(in srgb, var(--tier-record) 46%, transparent);
}
.tier-none      { background: transparent; color: var(--ink-faint); }

/* THE SAME SCALE AS INK ONLY, for figures that are Trophy Scores but are not
   chips — the big number in a profile's stat row, and the metric column on the
   trophy leaderboard.

   The profile's best score was hardcoded to --gold, so a 4% best and a 166%
   record were the same colour as each other and as nothing else on the site.
   A score is a position on a five-tier scale everywhere else it appears; there
   is no reason for it to stop being one because it is set in a larger size.

   A data attribute rather than the .tier-* classes because those carry a
   background and a border, which a stat figure must not have. */
.score-ink[data-tier="logged"]    { color: var(--tier-logged); }
.score-ink[data-tier="notable"]   { color: var(--tier-notable); }
.score-ink[data-tier="trophy"]    { color: var(--tier-trophy); }
.score-ink[data-tier="legendary"] { color: var(--tier-legendary); }
.score-ink[data-tier="record"]    { color: var(--tier-record); }
.score-ink[data-tier="none"]      { color: var(--ink-faint); }

.iucn { display: inline-flex; align-items: center; gap: .34rem; font-size: var(--t-xs); color: var(--ink-mute); }
.iucn::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.iucn[data-s="LC"] { color: var(--moss); }
.iucn[data-s="NT"] { color: #6f7f3c; }
.iucn[data-s="VU"] { color: var(--sand-deep); }
.iucn[data-s="EN"] { color: var(--rust); }
.iucn[data-s="CR"] { color: var(--clay); }
.iucn[data-s="DD"], .iucn[data-s="NE"] { color: var(--ink-faint); }

/* Rating bars step up in height as well as filling — a five-bar row that
   only changes colour is much harder to read at a glance than one that
   also changes shape. */
.rating { display: inline-flex; gap: 2px; align-items: flex-end; height: 14px; }
.rating i { width: 4px; border-radius: 1px; background: var(--line-strong); }
.rating i:nth-child(1) { height: 6px; }
.rating i:nth-child(2) { height: 8px; }
.rating i:nth-child(3) { height: 10px; }
.rating i:nth-child(4) { height: 12px; }
.rating i:nth-child(5) { height: 14px; }
.rating i.on { background: var(--sea); }

/* --------------------------------------------------------------------------
   12. Hero
   -------------------------------------------------------------------------- */
/* THE HERO IS A PANEL NOW, not a page treatment.
 *
 * It used to be open page: the headline sat directly on the water column with
 * the aquarium swimming behind it, and the section's only job was vertical
 * padding. That is an editorial construction — the page IS the artwork — and
 * it is the single thing that most made this read as a document rather than a
 * product.
 *
 * A contained panel does three things the open page could not: it gives the
 * headline a ground dark enough to hold 800-weight white type at any size, it
 * bounds the hero so the content below starts at a predictable place instead
 * of fading out of a gradient, and it lets the specimen plate sit on something
 * rather than float on moving water.
 *
 * The gradient is drawn from the WATER RAMP rather than from fixed colours, so
 * it re-tunes with the theme. Which stops to read is NOT the same on every
 * theme, and assuming it was is how this shipped with white text on a white
 * panel — see the Harbor override below. */
.hero { position: relative; padding-block: .5rem clamp(2rem, 4vw, 3rem); }
.hero-inner {
    position: relative;
    z-index: 3;
    overflow: hidden;
    isolation: isolate;
    border-radius: var(--r-xl);
    padding: clamp(2.4rem, 5vw, 4.2rem) clamp(1.5rem, 4vw, 3.4rem);
    background: linear-gradient(158deg, var(--w-900) 0%, var(--w-800) 46%, var(--w-700) 100%);
    color: #fff;
}
/* The one light source, top-right, so the panel is lit rather than flat. */
.hero-inner::after {
    content: "";
    position: absolute;
    z-index: -1;
    top: -38%;
    right: -14%;
    width: 62%;
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(125, 205, 240, .30), transparent 66%);
    pointer-events: none;
}

/* HARBOR'S RAMP RUNS THE OTHER WAY, and this override is the whole reason it
   needs one.
 *
 * The token contract at the top of this file says the ramp runs "light to
 * deep", w-050 to w-900. Spectrum does. Harbor does the OPPOSITE — its w-050
 * is #20394f and its w-900 is #f3f7fa — because on a dark theme the deep end
 * of the water is the light end of the page, and its depth bands flip their
 * label colour to match. That is coherent and it is also the one thing a rule
 * reading the ramp for a GROUND cannot ignore.
 *
 * Read literally, the panel above resolved to #f3f7fa on Harbor and put white
 * type on it: 1.08:1, which is not low contrast, it is invisible. Reading
 * Harbor's ramp from its own dark end gives the same panel at 8.3-11.9:1.
 *
 * Not a hard-coded navy, because the panel should still move if Harbor's
 * palette is ever retuned — it just has to move from the correct end. */
[data-theme="harbor"] .hero-inner {
    background: linear-gradient(158deg, var(--w-050) 0%, var(--w-100) 46%, var(--w-200) 100%);
}

/* AND THE SAME TRAP ONE RULE LATER. The inverted button below paints white and
   takes its LABEL from --w-900, which is the identical mistake in a second
   place: on Harbor that resolved to #f3f7fa, so the primary call to action was
   white-on-white while the headline beside it had already been fixed. Read
   from Harbor's dark end and it is 11.9:1. */
[data-theme="harbor"] .hero-actions .btn-primary { color: var(--w-050); }
[data-theme="harbor"] .hero-actions .btn-primary:hover { color: var(--w-050); }

/* White inside the panel, whatever the theme's ink is. */
.hero .display { max-width: 15ch; margin-block: 1.1rem 1.3rem; color: #fff; }
.hero .lead { font-size: clamp(1rem, .95rem + .35vw, 1.22rem); color: rgba(255, 255, 255, .76); }
.hero .kicker { color: rgba(255, 255, 255, .72); }
.hero .kicker::before { background: rgba(255, 255, 255, .42); }

/* THE PANEL'S WHITE STOPS AT THE PLATE. `.hero-inner` sets color:#fff for the
   headline, and colour inherits — so the specimen card's caption, which sits
   on cream paper inside the panel, was rendering white on near-white and had
   effectively vanished. The card owns its own ground, so it owns its own ink. */
.hero-specimen { color: var(--ink); }

.hero-actions { display: flex; gap: .75rem; flex-wrap: wrap; margin-top: 2.2rem; }

/* ON A DARK PANEL THE BUTTONS INVERT. The primary becomes the light one —
   white on a dark ground is the strongest affordance available — and the
   secondary is a hairline rather than a filled shape. */
.hero-actions .btn-primary {
    background: #fff;
    color: var(--w-900);
    border-color: transparent;
}
.hero-actions .btn-primary:hover { background: rgba(255, 255, 255, .88); color: var(--w-900); }
.hero-actions .btn-ghost,
.hero-actions .btn-secondary {
    background: transparent;
    color: #fff;
    border-color: rgba(255, 255, 255, .34);
}
.hero-actions .btn-ghost:hover,
.hero-actions .btn-secondary:hover { background: rgba(255, 255, 255, .12); color: #fff; }

.hero-grid { display: grid; gap: clamp(2rem, 5vw, 4rem); align-items: center; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 900px) {
    .hero-grid { grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); }
}

/* THE PLATE AND ITS LABEL ARE ONE OBJECT.
 *
 * The caption used to sit a centimetre below the card, on the open reef —
 * small grey type over moving water and a graticule, which is the one place on
 * this page where text cannot be read. It was also the second time the fish
 * was named, because the plate captions itself; that half is fixed in the
 * partial, and this is the other half.
 *
 * A specimen plate carries its label. The card now holds both, the label
 * separated by the same hairline rule the plate is drawn with, so the whole
 * assembly reads as one mounted object instead of a picture with a stray line
 * of text under it. */
.hero-specimen {
    position: relative;
    z-index: 2;
    /* The CARD, not the plate. The fish inside is a .species-media-plate and
       brings its own water; this is the mount the caption sits on, and the
       caption carries muted and faint text that reads on a card and would not
       on the deep end of a gradient. */
    background: var(--card);
    border: 1px solid var(--plate-rule);
    border-radius: var(--r-xl);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}
.hero-specimen .species-media {
    width: 100%;
    /* The card owns the frame now — a second border inside it was a double
       rule nobody asked for. */
    border: 0;
    border-radius: 0;
    box-shadow: none;
}
/* The hero's plate uses the shared swim below like every other one. */

/* --------------------------------------------------------------------------
   THE FISH SWIM
   --------------------------------------------------------------------------
   A drawing of a fish on a card is a specimen. The same drawing holding
   station in a current is an animal.

   WHY THIS IS FOUR LAYERS AND NOT ONE ANIMATION. An element can only run one
   `transform` at a time — a second animation touching transform replaces the
   first rather than composing with it — so a single keyframe set has to cram
   rise, drift, roll and flex into one curve, and a single curve is exactly
   what makes motion read as mechanical. Everything lands on the same beat
   because there is only one beat available.

   So the motion is split across the elements that are already nested:

     .index-thumb / .species-media-art   the wrapper — slow lateral DRIFT
     .fish-art                           the body    — PITCH and roll
     .fish-tail                          the caudal  — the BEAT
     .fish-fins                          dorsal/anal — a slow FLUTTER

   Four transforms, composed by the DOM instead of by a keyframe.

   AND FOUR PERIODS THAT DO NOT DIVIDE INTO EACH OTHER — 7.3s, 4.9s, 1.3s,
   2.1s. That is the whole trick. Harmonically related periods re-sync every
   few seconds and the eye catches the loop instantly; these drift against one
   another for minutes, so the fish never repeats a pose you just watched.

   THE TAIL LEADS THE BODY, which is the right way round and looks wrong if you
   reverse it: thrust comes from the tail and the body answers it. The beat is
   also the fastest thing on screen at 1.3s, because a fish that rolls faster
   than it kicks reads as a hooked one.
   -------------------------------------------------------------------------- */

/* 1 — DRIFT. The whole animal easing around a point, never travelling. */
.species-media-art { animation: fish-drift 7.3s ease-in-out infinite; }
.index-thumb       { animation: fish-drift-sm 7.3s ease-in-out infinite; }

/* 2 — PITCH. The body angling into and out of the drift. */
.species-media-art .fish-art,
.index-thumb .fish-art {
    animation: fish-pitch 4.9s ease-in-out infinite;
    will-change: transform;
}
.index-thumb .fish-art { animation-name: fish-pitch-sm; }

/* 3 — THE BEAT.
   `transform-box: fill-box` is what makes this species-agnostic. Every fish is
   drawn to its own viewBox — a turbot is 420x808, a barracuda 420x210 — so a
   hard-coded origin is right for one archetype and tears the fin off the body
   for the next. fill-box makes the origin the element's OWN bounding box, and
   "left center" on a caudal fin is where it joins the body, whatever shape it
   is. Measured in the browser it resolves to 0px 74.9px on the shark. */
.species-media-art .fish-tail,
.index-thumb .fish-tail {
    transform-box: fill-box;
    transform-origin: left center;
    /* A tail SNAPS through the stroke and GLIDES back. Symmetric easing is
       what made the first version read as a metronome. */
    animation: fish-beat 1.3s cubic-bezier(.36, .07, .3, 1) infinite;
}

/* 4 — FLUTTER. Dorsal and anal, trimming against the roll. Nearly invisible
   on its own and the reason the whole thing stops looking like one rigid
   shape being waggled. */
.species-media-art .fish-fins,
.index-thumb .fish-fins {
    transform-box: fill-box;
    transform-origin: center bottom;
    animation: fish-flutter 2.1s ease-in-out infinite;
}

@keyframes fish-drift {
    0%, 100% { transform: translate(0, 0); }
    25%      { transform: translate(-5px, -7px); }
    50%      { transform: translate(2px, -11px); }
    75%      { transform: translate(6px, -4px); }
}
@keyframes fish-drift-sm {
    0%, 100% { transform: translate(0, 0); }
    25%      { transform: translate(-1.2px, -1.6px); }
    50%      { transform: translate(.5px, -2.6px); }
    75%      { transform: translate(1.4px, -1px); }
}

@keyframes fish-pitch {
    0%, 100% { transform: rotate(0)      scaleY(1);    }
    30%      { transform: rotate(-1.6deg) scaleY(.985); }
    65%      { transform: rotate(1.3deg)  scaleY(1.01); }
}
@keyframes fish-pitch-sm {
    0%, 100% { transform: rotate(0)       scaleY(1);    }
    30%      { transform: rotate(-.85deg) scaleY(.992); }
    65%      { transform: rotate(.7deg)   scaleY(1.005);}
}

/* skewY as well as rotate: a caudal fin BENDS through its length rather than
   swinging as a rigid plate, and the skew is what reads as membrane. */
@keyframes fish-beat {
    0%, 100% { transform: rotate(0)      skewY(0);     }
    28%      { transform: rotate(5.5deg) skewY(3.4deg);}
    52%      { transform: rotate(1deg)   skewY(.6deg); }
    78%      { transform: rotate(-5deg)  skewY(-3deg); }
}

@keyframes fish-flutter {
    0%, 100% { transform: rotate(0)      skewX(0);      }
    40%      { transform: rotate(1.5deg) skewX(-1.6deg);}
    72%      { transform: rotate(-1.2deg) skewX(1.2deg);}
}

/* STAGGERED, or twenty-four fish beat in perfect unison and the table reads as
   one object breathing rather than as a shoal. Every layer is offset by a
   different amount per bucket, so rows differ in phase AND in the relationship
   between their own four movements. Six buckets; the eye cannot count past
   about four. */
.index tbody tr:nth-child(6n+2) .index-thumb { animation-delay: -1.2s; }
.index tbody tr:nth-child(6n+2) .fish-art    { animation-delay: -0.8s; }
.index tbody tr:nth-child(6n+2) .fish-tail   { animation-delay: -0.2s; }
.index tbody tr:nth-child(6n+2) .fish-fins   { animation-delay: -0.5s; }

.index tbody tr:nth-child(6n+3) .index-thumb { animation-delay: -2.4s; }
.index tbody tr:nth-child(6n+3) .fish-art    { animation-delay: -1.7s; }
.index tbody tr:nth-child(6n+3) .fish-tail   { animation-delay: -0.5s; }
.index tbody tr:nth-child(6n+3) .fish-fins   { animation-delay: -1.1s; }

.index tbody tr:nth-child(6n+4) .index-thumb { animation-delay: -3.6s; }
.index tbody tr:nth-child(6n+4) .fish-art    { animation-delay: -2.6s; }
.index tbody tr:nth-child(6n+4) .fish-tail   { animation-delay: -0.8s; }
.index tbody tr:nth-child(6n+4) .fish-fins   { animation-delay: -1.6s; }

.index tbody tr:nth-child(6n+5) .index-thumb { animation-delay: -4.8s; }
.index tbody tr:nth-child(6n+5) .fish-art    { animation-delay: -3.4s; }
.index tbody tr:nth-child(6n+5) .fish-tail   { animation-delay: -1.0s; }
.index tbody tr:nth-child(6n+5) .fish-fins   { animation-delay: -0.3s; }

.index tbody tr:nth-child(6n)   .index-thumb { animation-delay: -6.0s; }
.index tbody tr:nth-child(6n)   .fish-art    { animation-delay: -4.2s; }
.index tbody tr:nth-child(6n)   .fish-tail   { animation-delay: -1.15s; }
.index tbody tr:nth-child(6n)   .fish-fins   { animation-delay: -1.9s; }

/* Motion is a flourish here and never information, so it is the first thing
   to go when somebody has asked for less of it. */
@media (prefers-reduced-motion: reduce) {
    .species-media-art,
    .index-thumb,
    .species-media-art .fish-art,
    .index-thumb .fish-art,
    .species-media-art .fish-tail,
    .index-thumb .fish-tail,
    .species-media-art .fish-fins,
    .index-thumb .fish-fins { animation: none; }
}

.hero-caption {
    margin: 0;
    padding: .85rem clamp(1rem, 3vw, 1.4rem) .95rem;
    border-top: 1px solid var(--plate-rule);
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    flex-wrap: wrap;
}
/* The attribution, when a photograph carries one, belongs on the mount too —
   and quieter than the label it sits under. */
.hero-specimen .image-credit {
    margin: 0;
    padding: 0 clamp(1rem, 3vw, 1.4rem) .8rem;
    color: var(--plate-ink);
    opacity: .75;
}

/* THE BOARD PICKER — eight ladders, and eight is the whole point.
 *
 * It was `auto-fit` with a 210px minimum, which at a desktop width fits five
 * across and leaves the last three in a half-empty row. A ragged final row is
 * the difference between a set of options and a pile of them, and this is the
 * first thing on the page: it is what says whether the eight boards were
 * designed or accumulated.
 *
 * Eight divides by four and by two, so the grid is pinned to those rather than
 * left to arithmetic — 4x2 on a desktop, 2x4 on a tablet, a single column on a
 * phone. Every row is full at every width.
 */
.board-picker {
    display: grid;
    gap: .75rem;
    grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 560px)  { .board-picker { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .board-picker { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* --------------------------------------------------------------------------
   13. Stats
   -------------------------------------------------------------------------- */
/* Title block. Charts carry their survey data in a ruled panel in one
   corner — squared, hairline-divided, no shadow. */
.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(136px, 1fr));
    gap: 1px;
    background: var(--line-strong);
    border: 1.5px solid var(--ink);
    border-radius: 3px;
    overflow: hidden;
}
.stat { background: var(--card); padding: 1.3rem 1rem; text-align: center; }
.stat-value {
    font-family: var(--font-display);
    font-size: clamp(1.5rem, 1rem + 2vw, 2.3rem);
    font-weight: 600;
    letter-spacing: -.035em;
    line-height: 1;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.stat-label {
    font-size: var(--t-xs);
    text-transform: uppercase;
    letter-spacing: .14em;
    color: var(--ink-mute);
    margin-top: .55rem;
}
/* Soundings are printed in blue on a chart, so the headline figures are too */
.stat-value { color: var(--sea-deep); }

/* --------------------------------------------------------------------------
   14. Depth bands
   The ramp still runs light-to-dark, because that IS the information — but
   it now starts at a pale shallow tint rather than mid-blue.
   -------------------------------------------------------------------------- */
.depths { display: grid; gap: 2px; border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow-sm); }
.depth-band {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.2rem 1.5rem;
    transition: padding-left .34s var(--ease);
    overflow: hidden;
    color: #fff;
}
.depth-band:hover { padding-left: 2.2rem; }
.depth-band::before { content: ""; position: absolute; inset: 0; z-index: -1; background: var(--band, var(--w-500)); }

/* Depth tinting straight off the water ramp, so it re-tunes with the theme.
   Each band steps one stop darker, which is how a chart shades from shoal to
   deep — the ramp is doing the explaining before anyone reads a label. */
.depth-band[data-zone="surface"]      { --band: linear-gradient(96deg, var(--w-100), var(--w-200)); color: var(--ink); }
.depth-band[data-zone="reef"]         { --band: linear-gradient(96deg, var(--w-200), var(--w-300)); color: var(--ink); }
.depth-band[data-zone="epipelagic"]   { --band: linear-gradient(96deg, var(--w-300), var(--w-400)); color: var(--ink); }
.depth-band[data-zone="mesopelagic"]  { --band: linear-gradient(96deg, var(--w-400), var(--w-500)); color: #fff; }
.depth-band[data-zone="demersal"]     { --band: linear-gradient(96deg, var(--w-500), var(--w-600)); color: #fff; }
.depth-band[data-zone="bathypelagic"] { --band: linear-gradient(96deg, var(--w-700), var(--w-900)); color: #fff; }
/* Inland water reads as a green wash, not a blue one */
.depth-band[data-zone="freshwater"]   { --band: linear-gradient(96deg, var(--moss), var(--w-700)); color: #fff; }

/* On the spectrum theme the depth column becomes an actual descent through
   the visible spectrum — warm and bright at the surface where red light still
   reaches, cold violet-blue in the abyss where only short wavelengths get
   through. That is genuinely how light behaves in seawater, which is why it
   can be this colourful and still be telling the truth. */
[data-theme="spectrum"] .depth-band[data-zone="surface"]      { --band: linear-gradient(96deg, #ffe08a, #ffc85c); color: #4a3405; }
[data-theme="spectrum"] .depth-band[data-zone="reef"]         { --band: linear-gradient(96deg, #ff9f5e, #f4713f); color: #4a1c05; }
[data-theme="spectrum"] .depth-band[data-zone="epipelagic"]   { --band: linear-gradient(96deg, #4fc08a, #17a97e); color: #04331f; }
[data-theme="spectrum"] .depth-band[data-zone="mesopelagic"]  { --band: linear-gradient(96deg, #22a9c9, #0e86c4); color: #fff; }
[data-theme="spectrum"] .depth-band[data-zone="demersal"]     { --band: linear-gradient(96deg, #2f6fd0, #274fa8); color: #fff; }
[data-theme="spectrum"] .depth-band[data-zone="bathypelagic"] { --band: linear-gradient(96deg, #4b3aa8, #241b52); color: #fff; }
[data-theme="spectrum"] .depth-band[data-zone="freshwater"]   { --band: linear-gradient(96deg, #7fc242, #3f8a3a); color: #0d2b0a; }

/* Dark themes run the ramp the other way, so the text contrast flips */
[data-theme="harbor"] .depth-band[data-zone="mesopelagic"],
[data-theme="harbor"] .depth-band[data-zone="demersal"],
[data-theme="harbor"] .depth-band[data-zone="bathypelagic"],
[data-theme="harbor"] .depth-band[data-zone="freshwater"] { color: var(--paper); }

/* Sounding line: the hairline separating each depth contour */
.depth-band + .depth-band { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .28); }

.depth-name { font-family: var(--font-display); font-size: var(--t-md); font-weight: 600; }
.depth-meta { font-size: var(--t-xs); opacity: .78; }
.depth-count { font-variant-numeric: tabular-nums; font-size: var(--t-sm); opacity: .88; }

/* --------------------------------------------------------------------------
   15. Size comparison (species page)
   -------------------------------------------------------------------------- */
/* Set like a chart's scale bar panel — squared, ruled, on tinted water */
.scale-viz {
    position: relative;
    padding: 1.6rem 1.35rem 1.1rem;
    border-radius: 3px;
    background:
        linear-gradient(180deg, var(--w-100), var(--w-050)),
        repeating-linear-gradient(180deg, transparent 0 43px, rgba(16, 34, 43, .06) 43px 44px);
    border: 1px solid var(--line-strong);
    overflow: hidden;
}
.scale-stage { display: flex; align-items: flex-end; gap: clamp(1rem, 4vw, 3rem); min-height: 200px; }
.scale-item { display: flex; flex-direction: column; align-items: center; gap: .6rem; }
.scale-item.human svg { color: var(--ink-faint); }
.scale-label { font-size: var(--t-xs); color: var(--ink-mute); text-align: center; }
.scale-label b { display: block; color: var(--ink); font-size: var(--t-sm); font-weight: 600; }

.scale-ruler {
    display: flex; justify-content: space-between;
    margin-top: .8rem; padding-top: .55rem;
    border-top: 1px solid var(--line);
    font-size: var(--t-xs); color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   16. Seasonality
   -------------------------------------------------------------------------- */
/* Seasonality. Gridlines behind the bars turn it from a decorative shape
   into something you can read a value off — the whole difference between a
   sparkline and a chart. */
.seasons {
    position: relative;
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: 4px;
    align-items: end;
    height: 116px;
    padding-top: 4px;
    background-image: repeating-linear-gradient(
        to top, transparent 0 24px, var(--line) 24px, var(--line) 25px);
    background-size: 100% 100%;
    background-position: 0 -18px;
}
.season-col { display: flex; flex-direction: column; align-items: center; gap: .35rem; height: 100%; justify-content: flex-end; }
.season-bar {
    width: 100%;
    border-radius: 3px 3px 0 0;
    background: var(--w-400);
    min-height: 3px;
    transition: height .7s var(--ease-back), background .2s var(--ease);
}
.season-col:hover .season-bar { background: var(--sea); }
/* Peak months use the record colour — the same mark used everywhere else
   for "this is the one that matters". */
.season-col[data-peak="true"] .season-bar { background: var(--mark); }
.season-label {
    font-size: 10px;
    color: var(--ink-faint);
    letter-spacing: .04em;
    font-variant-numeric: tabular-nums;
}
.season-col[data-peak="true"] .season-label { color: var(--mark); font-weight: 700; }

/* --------------------------------------------------------------------------
   17. Tables
   -------------------------------------------------------------------------- */
.table-wrap { overflow-x: auto; border-radius: var(--r); border: 1px solid var(--line); background: var(--card); box-shadow: var(--shadow-sm); }
.table-wrap::-webkit-scrollbar { height: 8px; }
.table-wrap::-webkit-scrollbar-thumb { background: var(--w-200); border-radius: 4px; }

table.data { font-size: var(--t-sm); min-width: 640px; }
table.data thead th {
    position: sticky; top: 0; z-index: 2;
    text-align: left;
    padding: .8rem 1rem;
    font-size: var(--t-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: var(--ink-mute);
    background: var(--paper-warm);
    border-bottom: 1px solid var(--line-strong);
    white-space: nowrap;
}
table.data thead th a { color: inherit; display: inline-flex; align-items: center; gap: .3rem; }
table.data thead th a:hover { color: var(--sea); }

table.data tbody td { padding: .72rem 1rem; border-bottom: 1px solid var(--line); vertical-align: middle; }
table.data tbody tr { transition: background .15s var(--ease); }
table.data tbody tr:hover { background: var(--w-050); }
table.data tbody tr:last-child td { border-bottom: none; }
table.data .num-cell { text-align: right; font-variant-numeric: tabular-nums; }
table.data .primary-cell { font-weight: 600; }
table.data .primary-cell a:hover { color: var(--sea); }

table.data tbody tr[data-rank="1"] { background: linear-gradient(90deg, var(--gold-wash), transparent 42%); }
table.data tbody tr[data-rank="2"] { background: linear-gradient(90deg, rgba(223, 228, 230, .5), transparent 42%); }
table.data tbody tr[data-rank="3"] { background: linear-gradient(90deg, rgba(221, 168, 120, .18), transparent 42%); }

/* --------------------------------------------------------------------------
   18. Forms
   -------------------------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: .4rem; }
.field > label { font-size: var(--t-sm); font-weight: 500; color: var(--ink-soft); }
.field-hint { font-size: var(--t-xs); color: var(--ink-faint); }
.field-error { font-size: var(--t-xs); color: var(--clay); }

.input, .select, .textarea {
    width: 100%;
    padding: .72rem .9rem;
    border-radius: var(--r-sm);
    background: var(--card);
    border: 1px solid var(--line-strong);
    color: var(--ink);
    font-size: var(--t-base);
    transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--ink-faint); }
.input:focus, .select:focus, .textarea:focus {
    outline: none;
    border-color: var(--sea);
    box-shadow: 0 0 0 3px var(--sea-wash);
}
.textarea { resize: vertical; min-height: 100px; line-height: 1.55; }

.select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%236b858b' stroke-width='1.6' d='M1 1.5l5 5 5-5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right .8rem center;
    background-size: 11px;
    padding-right: 2.2rem;
}

.input-group { display: flex; }
.input-group .input { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.input-group .input-suffix {
    display: grid; place-items: center;
    padding-inline: .85rem;
    border: 1px solid var(--line-strong);
    border-left: none;
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
    background: var(--paper-warm);
    color: var(--ink-mute);
    font-size: var(--t-sm);
}

.check { display: flex; align-items: flex-start; gap: .6rem; cursor: pointer; }
.check input { margin-top: .25rem; accent-color: var(--sea); width: 16px; height: 16px; }

.radio-cards { display: grid; gap: .5rem; }
.radio-card {
    display: flex; align-items: flex-start; gap: .7rem;
    padding: .82rem 1rem;
    border-radius: var(--r);
    border: 1px solid var(--line-strong);
    background: var(--card);
    cursor: pointer;
    transition: border-color .2s var(--ease), background .2s var(--ease);
}
.radio-card:hover { border-color: var(--sea); }
.radio-card input { margin-top: .2rem; accent-color: var(--sea); }
.radio-card:has(input:checked) { border-color: var(--sea); background: var(--sea-wash); }
.radio-card b { display: block; font-size: var(--t-sm); }
.radio-card span { font-size: var(--t-xs); color: var(--ink-mute); }

.form-grid { display: grid; gap: var(--gap); grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); }
.form-section { padding-top: 1.5rem; border-top: 1px solid var(--line); }
.form-section:first-child { padding-top: 0; border-top: none; }
.form-section > h3 { margin-bottom: 1rem; font-size: var(--t-md); }

.combo { position: relative; }
.combo-list {
    position: absolute;
    inset-inline: 0;
    top: calc(100% + .25rem);
    z-index: 40;
    max-height: 290px;
    overflow-y: auto;
    border-radius: var(--r);
    background: var(--card);
    border: 1px solid var(--line-strong);
    box-shadow: var(--shadow);
    display: none;
}
.combo-list[data-open="true"] { display: block; }
.combo-option {
    display: flex; justify-content: space-between; align-items: center; gap: .75rem;
    padding: .6rem .85rem;
    cursor: pointer;
    font-size: var(--t-sm);
}
.combo-option:hover, .combo-option[aria-selected="true"] { background: var(--sea-wash); }
.combo-option .sci { font-size: var(--t-xs); }

/* The English name of a fish, shown to readers browsing in another language.
   Set quieter than the local name and louder than the Latin: it is a hint,
   not the label, but it is the string a lot of people will recognise first. */
.combo-en {
    margin-left: .45rem;
    font-size: var(--t-xs);
    color: var(--ink-mute);
}
.name-en {
    display: block;
    font-size: var(--t-xs);
    font-style: normal;
    color: var(--ink-mute);
}

/* --------------------------------------------------------------------------
   19. Filters
   -------------------------------------------------------------------------- */
.filters {
    display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end;
    padding: 1rem;
    border-radius: var(--r);
    background: var(--paper-warm);
    border: 1px solid var(--line);
}
.filters .field { flex: 1 1 150px; min-width: 0; }
.filters .field > label { font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--ink-mute); }
.filters .input, .filters .select { padding: .55rem .75rem; font-size: var(--t-sm); }

.chip-filters { display: flex; flex-wrap: wrap; gap: .4rem; }
.chip-filter {
    padding: .35rem .82rem;
    border-radius: 999px;
    font-size: var(--t-xs);
    font-weight: 500;
    border: 1px solid var(--line-strong);
    background: var(--card);
    color: var(--ink-soft);
    transition: all .2s var(--ease);
}
.chip-filter:hover { border-color: var(--sea); color: var(--ink); }
.chip-filter[aria-pressed="true"], .chip-filter.active {
    background: var(--sea); border-color: var(--sea); color: #fff;
}

/* --------------------------------------------------------------------------
   20. Profile
   -------------------------------------------------------------------------- */
.profile-head { display: flex; gap: clamp(1rem, 3vw, 2rem); align-items: center; flex-wrap: wrap; }

/* ── The profile banner ───────────────────────────────────────────────────
   A 3:1 strip at the top of an angler's page, and the identity row hanging
   across its lower edge.

   THE ASPECT RATIO IS DECLARED, not left to the image. A banner is one of the
   last things to arrive on the page — it is lazy-loaded and it is the largest
   asset on a profile — so without a reserved box the whole page jumps down by
   200 px when it lands. `aspect-ratio` with the intrinsic width/height on the
   <img> means the space is held from first paint.

   3:1 is BannerArt's viewBox and the ratio Banner crops uploads to, so the
   generated banner and an uploaded one occupy exactly the same box and nothing
   reflows when somebody uploads one.

   The image is `cover`, not `contain`. A short letterboxed banner with bars
   down the sides reads as a broken upload; a crop reads as a crop. */
.profile-card {
    /* --r-lg, matching .panel — every other card on the profile is a .panel
       and a card with a tighter radius than its neighbours reads as a
       different kind of thing. */
    border-radius: var(--r-lg);
    border: 1px solid var(--line);
    background: var(--card);
    overflow: hidden;
}
.profile-banner {
    aspect-ratio: 3 / 1;
    /* EXPLICIT, because the min-height below would otherwise set the WIDTH.
       A box with a ratio and no definite width resolves the ratio the other
       way round: the 112px floor became 336px of width, and inside the account
       settings panel — whose padding leaves 304px on a 375px phone — the strip
       hung 32px past the card's edge. Stating the width makes the ratio
       compute the height, which is the direction it was always meant to run.
       The profile page never showed it because .profile-card is wide enough
       that the floor never engages. */
    width: 100%;
    /* A floor, so the strip does not collapse to a line on a narrow phone
       where 3:1 of 320 px is under 110 px and the art has nothing to show. */
    min-height: 112px;
    background: var(--paper-sunk);
    overflow: hidden;
}
.profile-banner img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ONLY THE AVATAR RIDES UP, not the row it is in.
   
   The first version lifted the whole identity row with a negative margin,
   which is the obvious way to do it and put the angler's NAME on top of the
   banner — dark ink on dark water, unreadable, and unreadable differently for
   every angler because the banner is generated per account. The name has to
   sit on the card.
   
   So the panel stays below the join and the picture alone reaches back into
   it. Negative margin rather than absolute positioning, so the avatar is still
   in the flow and a wrapped button or a long display name pushes the panel
   taller instead of overlapping what is under it. */
.profile-head-under {
    position: relative;
    /* .panel's own frame is switched off: this one is nested inside
       .profile-card, which already draws the border, the background and the
       radius. Leaving the shadow on drew a second card edge a few pixels
       inside the first. */
    border: 0;
    background: transparent;
    box-shadow: none;
    align-items: flex-start;
}
.profile-head-under .ident {
    margin-top: clamp(-3.5rem, -7vw, -2.25rem);
    /* A rim against the banner, or a dark photograph behind a dark-haired
       angler becomes one shape. */
    box-shadow: 0 0 0 4px var(--card), 0 6px 18px rgba(var(--shadow-rgb), .28);
    border-radius: 50%;
}
/* The text column is the tallest thing in the row and the buttons should meet
   its top edge, not float against a raised avatar. */
.profile-head-under > .flex-1 { padding-top: .35rem; }

@media (max-width: 620px) {
    /* Less lift on a phone: the row wraps, and at full lift the avatar clears
       the card entirely and sits on the banner like a sticker. */
    .profile-head-under .ident { margin-top: -1.75rem; }
}

/* ── The identity mark ────────────────────────────────────────────────────
   A photograph of the angler, everywhere they are listed.

   THIS USED TO BE THE EMBLEM. Boards and posts drew the rank badge in the
   avatar slot, which meant a leaderboard was fifty variations on the same gold
   disc and you could not tell one row from another without reading every name.
   A list of people should be scannable by face. The badge answers a different
   question and now lives in one place — the rank panel on an angler's own page
   — rather than being repeated beside every name on the site.

   --ident-metal is set inline by partials/angler-avatar.php to the rank's own
   metal, and tints the initial shown when there is no photograph. */
.ident {
    position: relative;
    display: inline-block;
    flex: none;
    line-height: 0;
    text-decoration: none;
}
.avatar {
    width: clamp(64px, 12vw, 104px);
    aspect-ratio: 1;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid var(--card);
    box-shadow: 0 0 0 1px var(--line-strong), var(--shadow-sm);
    background: linear-gradient(150deg, var(--w-300), var(--w-600));
    display: grid;
    place-items: center;
    font-family: var(--font-display);
    font-size: 1.75rem;
    font-weight: 600;
    color: #fff;
    flex-shrink: 0;
}
.avatar-sm { width: 32px; font-size: .85rem; border-width: 1px; }
.avatar-xs { width: 24px; font-size: .7rem; border-width: 1px; }

/* An account with no photograph still wears its rank: the initial sits on a
   wash of the angler's own metal rather than the same house blue for
   everybody. Enough tint to be a colour, not so much that white type on it
   stops clearing contrast. */
.ident .avatar-initial {
    background:
        linear-gradient(150deg,
            color-mix(in srgb, var(--ident-metal, var(--w-400)) 55%, #0b1118 18%),
            color-mix(in srgb, var(--ident-metal, var(--w-600)) 34%, #0b1118 42%));
    color: #fff;
    text-shadow: 0 1px 2px rgba(11, 17, 24, .38);
}

/* Sizes match the width table in angler-avatar.php, so the <img> is handed its
   intrinsic size and a row never reflows as pictures land. */
.ident-xs .avatar { width: 24px; font-size: .7rem; border-width: 1px; }
.ident-sm .avatar { width: 36px; font-size: .95rem; border-width: 1px; }
.ident-md .avatar { width: 48px; font-size: 1.15rem; border-width: 1px; }
.ident-lg .avatar { width: 72px; font-size: 1.5rem; }
.ident-xl .avatar { width: 104px; font-size: 2.1rem; }

@media (prefers-reduced-motion: no-preference) {
    .ident[href] .avatar { transition: box-shadow .18s var(--ease, ease); }
    .ident[href]:hover .avatar {
        box-shadow: 0 0 0 2px var(--ident-metal, var(--sea)), var(--shadow-sm);
    }
}

.passport-bar { height: 10px; border-radius: 999px; background: var(--paper-sunk); overflow: hidden; border: 1px solid var(--line); }
.passport-fill {
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--sea), var(--gold));
    transition: width 1.1s var(--ease-back);
}

.slam-grid { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fill, minmax(min(190px, 100%), 1fr)); }
.slam { padding: .82rem .95rem; border-radius: var(--r); border: 1px solid var(--line); background: var(--paper-warm); }
.slam-name { font-size: var(--t-sm); font-family: var(--font-display); font-style: italic; }
.slam-bar { height: 5px; border-radius: 999px; background: var(--paper-sunk); margin-top: .5rem; overflow: hidden; }
.slam-bar i { display: block; height: 100%; background: var(--sea); border-radius: 999px; }
.slam[data-complete="true"] .slam-bar i { background: var(--gold); }

.badges { display: flex; flex-wrap: wrap; gap: .5rem; }
.badge-pill {
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .4rem .8rem;
    border-radius: 999px;
    font-size: var(--t-xs);
    font-weight: 600;
    border: 1px solid var(--line-strong);
    background: var(--card);
    color: var(--ink-soft);
}
.badge-pill[data-tier="silver"]   { border-color: #b3bfc3; color: #56666b; }
.badge-pill[data-tier="gold"]     { border-color: rgba(156, 47, 102, .4); color: var(--gold); background: var(--gold-wash); }
.badge-pill[data-tier="platinum"] { border-color: var(--sea); color: var(--sea); background: var(--sea-wash); }

.sparkline {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: 60px;
    padding-bottom: 1px;
    border-bottom: 1px solid var(--line-strong);
}
.sparkline i {
    flex: 1;
    min-height: 2px;
    border-radius: 2px 2px 0 0;
    background: var(--w-300);
    transition: background .2s var(--ease);
}
.sparkline:hover i { background: var(--w-200); }
.sparkline i:hover { background: var(--sea) !important; }

/* --------------------------------------------------------------------------
   21. Visual size table
   A table where the fish column IS the comparison — every silhouette drawn
   to the same scale, so the column reads as a chart.
   -------------------------------------------------------------------------- */
.size-table { width: 100%; font-size: var(--t-sm); }
.size-table th {
    text-align: left;
    padding: .7rem 1rem;
    font-size: var(--t-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: var(--ink-mute);
    border-bottom: 1px solid var(--line-strong);
    white-space: nowrap;
}
.size-table td { padding: .55rem 1rem; border-bottom: 1px solid var(--line); vertical-align: middle; }
.size-table tr:last-child td { border-bottom: none; }
.size-table tr:hover { background: var(--w-050); }
.size-table .num-cell { text-align: right; font-variant-numeric: tabular-nums; }

/* The scale column.
   The icon lives in its own fixed-size cell beside this; here is just the
   proportional bar, on one shared scale across every row. A bar has a
   straight edge to read a length against — a fish silhouette does not, which
   is why scaling the illustration made a worse chart AND an unreadable
   picture of the smallest fish in the log. */
.size-icon-cell { width: 1%; padding-right: .35rem; }
.size-icon { width: 76px; height: 52px; }

.size-bar {
    position: relative;
    width: min(42vw, 380px);
    min-width: 140px;
    height: 12px;
    display: flex;
    align-items: center;
    border-radius: 6px;
    background: var(--surface);
    overflow: hidden;
}
.size-bar-fill {
    position: relative;
    z-index: 1;
    height: 100%;
    border-radius: 6px;
    background: linear-gradient(90deg, var(--h3), var(--h2));
    transition: filter .25s var(--ease);
}
.size-table tr:hover .size-bar-fill { filter: brightness(1.12); }

/* A metre rule behind the bar. This is what turns the column from a pretty
   bar chart into something you can read a measurement off. */
.size-bar-track {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background: repeating-linear-gradient(90deg,
                transparent 0 calc(var(--tick, 40px) - 1px),
                rgba(12, 31, 36,.14) calc(var(--tick, 40px) - 1px),
                rgba(12, 31, 36,.14) var(--tick, 40px));
}

@media (max-width: 720px) {
    .size-bar { width: 100%; min-width: 120px; }
}

/* --------------------------------------------------------------------------
   23. Map
   -------------------------------------------------------------------------- */
/* A map wants height. clamp(460, 72vh, 780) sounds generous and isn't: on a
   1080p laptop 72vh is about 700px of which the browser chrome and the page
   header eat a third, and the world at zoom 2 ends up in a letterbox. The
   floor now clears a phone's fold and the ceiling is tall enough that an
   ocean basin fits on a desktop without scrolling. */
/* z-index: 0 is doing real work here — it is not decoration.
   Leaflet gives its own panes z-index 400 to 700, and its controls 800+.
   Without a stacking context on this element those numbers compete directly
   with .masthead (z-index: 100), so the map tiles and every popup rendered
   OVER the sticky navbar as soon as the page scrolled.
   Setting z-index on a positioned element creates a stacking context, which
   traps Leaflet's whole 400-800 range inside this box. The map can now order
   its own layers however it likes and still sit under the site chrome.
   `isolation: isolate` would do the same thing; z-index: 0 is understood by
   more browsers and is what the overflow/border-radius here already implies. */
.map-shell { position: relative; z-index: 0; height: clamp(560px, 82vh, 1020px); border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--line-strong); box-shadow: var(--shadow-sm); }
#map { position: absolute; inset: 0; background: var(--w-050); }

.map-panel {
    position: absolute; top: 1rem; left: 1rem; z-index: 500;
    width: min(280px, calc(100% - 2rem));
    padding: 1rem;
    border-radius: var(--r);
    background: rgba(255, 253, 249, .96);
    backdrop-filter: blur(12px);
    border: 1px solid var(--line-strong);
    box-shadow: var(--shadow);
}
.map-panel .field + .field { margin-top: .7rem; }
.map-status {
    position: absolute; top: 1rem; left: 50%; transform: translateX(-50%); z-index: 500;
    white-space: nowrap; box-shadow: var(--shadow-sm);
    font-size: var(--t-xs); color: var(--ink-soft);
    background: color-mix(in srgb, var(--card) 92%, transparent);
    padding: .4rem .8rem; border-radius: 999px; border: 1px solid var(--line);
    backdrop-filter: blur(6px);
}

/* Map legend. A map of coloured dots with no key is decoration; the same map
   with a key is data. */
.map-legend {
    position: absolute;
    right: 1rem;
    bottom: 1rem;
    z-index: 500;
    padding: .7rem .85rem;
    border-radius: var(--r);
    background: color-mix(in srgb, var(--card) 94%, transparent);
    border: 1px solid var(--line-strong);
    backdrop-filter: blur(10px);
    box-shadow: var(--shadow-sm);
    font-size: var(--t-xs);
    max-width: 190px;
}
.map-legend h4 {
    font-family: var(--font-body);
    font-size: var(--t-xs);
    text-transform: uppercase;
    letter-spacing: .12em;
    color: var(--ink-mute);
    margin-bottom: .5rem;
}
.map-legend ul { display: grid; gap: .3rem; }
.map-legend li { display: flex; align-items: center; gap: .5rem; color: var(--ink-soft); }
.map-legend i {
    width: 11px; height: 11px; border-radius: 50%;
    border: 1.5px solid var(--card);
    box-shadow: 0 0 0 1px var(--line-strong);
    flex-shrink: 0;
}
@media (max-width: 780px) { .map-legend { display: none; } }

/* The real map should sit in the same colour world as the rest of the page */
.leaflet-container { background: var(--w-100) !important; font-family: var(--font-body) !important; }
.leaflet-popup-content-wrapper {
    background: var(--card) !important;
    color: var(--ink) !important;
    border-radius: var(--r) !important;
    border: 1px solid var(--line-strong);
    box-shadow: var(--shadow) !important;
}
.leaflet-popup-tip { background: var(--card) !important; }
.leaflet-popup-content { margin: .85rem 1rem !important; font-size: var(--t-sm) !important; line-height: 1.5 !important; }
.leaflet-popup-close-button { color: var(--ink-mute) !important; }
.leaflet-control-zoom a { background: var(--card) !important; color: var(--ink) !important; border-color: var(--line) !important; }
.leaflet-control-zoom a:hover { background: var(--sea-wash) !important; }
.leaflet-control-attribution { background: rgba(255, 253, 249, .85) !important; color: var(--ink-mute) !important; font-size: 10px !important; }
.leaflet-control-attribution a { color: var(--sea) !important; }

/* ---- The marks (see map.js: catchIcon, charterIcon, the cluster bubble) --- */
.fr-mark { background: none; border: 0; }

/* A catch: a pin with a fish in it, in its Trophy Score tier's colour. */
.fr-pin {
    position: relative;
    width: 100%; height: 100%;
    transform-origin: 50% 100%;
    transition: transform .16s ease;
    filter: drop-shadow(0 3px 3px rgba(6, 30, 44, .32)) drop-shadow(0 1px 1px rgba(6, 30, 44, .25));
}
.fr-pin svg { display: block; width: 100%; height: 100%; overflow: visible; }
.fr-pin .pb { fill: var(--pin); stroke: #fff; stroke-width: 2.4; }
.fr-pin .pg path { fill: #fff; }
.fr-pin .pg .eye { fill: var(--pin); }
/* Released: hollow, the fish in the tier colour — the map shows a fishery's
   culture at a glance. */
.fr-pin.rel .pb { fill: #fff; stroke: var(--pin); stroke-width: 3; }
.fr-pin.rel .pg path { fill: var(--pin); }
.fr-pin.rel .pg .eye { fill: #fff; }
.fr-pin .star circle { fill: #f2c34a; stroke: #fff; stroke-width: 1.6; }
.fr-pin .star path { fill: #fff; }
.leaflet-marker-icon:hover .fr-pin,
.leaflet-marker-icon:focus-visible .fr-pin { transform: scale(1.14); }
/* This catch, on its own page. */
.fr-pin.focus { transform: scale(1.18); }
.fr-pin.focus::after {
    content: ""; position: absolute; left: 50%; bottom: -6px; width: 26px; height: 10px; margin-left: -13px;
    border-radius: 50%; border: 2px solid var(--pin); opacity: .8;
    animation: fr-ping 1.8s ease-out infinite;
}
@keyframes fr-ping { from { transform: scale(.4); opacity: .9; } to { transform: scale(2.2); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .fr-pin.focus::after { animation: none; } }

/* A group of catches: a bubble, haloed, warmer as it fills up. */
.fr-cluster {
    --c1: #2a9fd6; --c2: #0a6e9e; --c3: #06496d; --halo: 10, 110, 158;
    display: grid; place-items: center;
    border-radius: 50%;
    color: #fff; font-weight: 800; letter-spacing: -.01em;
    background: radial-gradient(circle at 34% 28%, var(--c1), var(--c2) 58%, var(--c3));
    border: 2px solid rgba(255, 255, 255, .95);
    box-shadow: 0 0 0 5px rgba(var(--halo), .24), 0 0 0 10px rgba(var(--halo), .1), 0 4px 12px rgba(6, 30, 44, .38);
    text-shadow: 0 1px 2px rgba(0, 0, 0, .3);
    transition: transform .16s ease, box-shadow .16s ease;
    cursor: pointer;
}
.fr-cluster.warm { --c1: #f0b44e; --c2: #d9851f; --c3: #9a5a12; --halo: 217, 133, 31; }
.fr-cluster.hot  { --c1: #f08a5e; --c2: #d9573b; --c3: #962d1d; --halo: 217, 87, 59; }
.leaflet-marker-icon:hover .fr-cluster { transform: scale(1.08); box-shadow: 0 0 0 6px rgba(var(--halo), .3), 0 0 0 13px rgba(var(--halo), .12), 0 6px 14px rgba(6, 30, 44, .4); }

/* A charter: a tag with a boat in it — not a pin, because it is not a catch. */
.fr-charter { width: 100%; height: 100%; transform-origin: 50% 100%; transition: transform .16s ease;
              filter: drop-shadow(0 3px 3px rgba(6, 30, 44, .3)); }
.fr-charter svg { display: block; width: 100%; height: 100%; }
.fr-charter .cb { fill: var(--h1, #7d5cd6); stroke: #fff; stroke-width: 2.2; }
.fr-charter .cg path { fill: #fff; }
.fr-charter .cg .mast { fill: none; stroke: #fff; stroke-width: 1.7; stroke-linecap: round; }
.fr-charter.off .cb { fill: #8a9ca8; }
.fr-charter.off { opacity: .85; }
.leaflet-marker-icon:hover .fr-charter { transform: scale(1.12); }

/* Mini versions of the same marks, for the legend. */
.lg-pin { width: 13px; height: 17px; flex: none; overflow: visible; }
.lg-pin .pb { fill: var(--pin); stroke: #fff; stroke-width: 3; }
.lg-pin .pg path { fill: #fff; }
.lg-pin.rel .pb { fill: #fff; stroke: var(--pin); stroke-width: 3.6; }
.lg-pin.rel .pg path { fill: var(--pin); }
.lg-tag { width: 15px; height: 17px; flex: none; }
.lg-tag .cb { fill: var(--h1, #7d5cd6); stroke: #fff; stroke-width: 2.4; }
.lg-tag .cg path { fill: #fff; }
.lg-tag .cg .mast { fill: none; stroke: #fff; stroke-width: 1.8; }
.lg-bubble { width: 15px; height: 15px; flex: none; border-radius: 50%; border: 1.5px solid #fff;
             background: radial-gradient(circle at 34% 28%, #2a9fd6, #0a6e9e 58%, #06496d);
             box-shadow: 0 0 0 2px rgba(10, 110, 158, .22); }

/* --------------------------------------------------------------------------
   24. Misc
   -------------------------------------------------------------------------- */
.pagination { display: flex; gap: .35rem; flex-wrap: wrap; justify-content: center; align-items: center; }
.pagination a, .pagination span {
    min-width: 38px; height: 38px;
    display: grid; place-items: center;
    padding-inline: .6rem;
    border-radius: var(--r-sm);
    font-size: var(--t-sm);
    border: 1px solid var(--line);
    background: var(--card);
    color: var(--ink-soft);
    transition: all .18s var(--ease);
}
.pagination a:hover { border-color: var(--sea); color: var(--sea); }
.pagination [aria-current="page"] { background: var(--sea); border-color: var(--sea); color: #fff; font-weight: 600; }
.pagination .gap { border: none; background: none; color: var(--ink-faint); }

.flash {
    display: flex; align-items: center; gap: .7rem;
    padding: .85rem 1.1rem;
    border-radius: var(--r);
    font-size: var(--t-sm);
    border: 1px solid;
    animation: slide-in .4s var(--ease-back);
}
.flash-success { background: #eaf6ec; border-color: rgba(74, 124, 78, .35); color: #2e5c33; }
.flash-error   { background: #fbeced; border-color: rgba(176, 74, 74, .35); color: #83302f; }
.flash-info    { background: var(--sea-wash); border-color: rgba(18, 128, 123, .3); color: var(--sea-deep); }

@keyframes slide-in { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }

.empty { text-align: center; padding: clamp(2.5rem, 7vw, 4.5rem) 1.5rem; color: var(--ink-mute); }
.empty svg { width: 74px; margin: 0 auto 1.3rem; opacity: .35; color: var(--sea); }
.empty h1, .empty h3 { color: var(--ink-soft); margin-bottom: .5rem; }

.breadcrumbs { display: flex; gap: .4rem; align-items: center; font-size: var(--t-xs); color: var(--ink-mute); flex-wrap: wrap; }
.breadcrumbs a:hover { color: var(--sea); }
.breadcrumbs li + li::before { content: "/"; margin-right: .4rem; color: var(--ink-faint); }
.breadcrumbs li { display: flex; align-items: center; }

.divider { height: 1px; background: linear-gradient(90deg, transparent, var(--line-strong), transparent); border: none; }

.fact-list { display: grid; gap: 0; border-radius: var(--r); overflow: hidden; border: 1px solid var(--line); background: var(--card); }
.fact { display: flex; justify-content: space-between; gap: 1rem; padding: .72rem 1rem; font-size: var(--t-sm); border-bottom: 1px solid var(--line); }
.fact:last-child { border-bottom: none; }
.fact dt { color: var(--ink-mute); }
.fact dd { text-align: right; font-weight: 500; font-variant-numeric: tabular-nums; }

/* Record block, set like a chart caution box: magenta rule, tinted ground,
   squared corners. Charts box the things you must not ignore. */
.record-block {
    position: relative;
    padding: 1.4rem 1.55rem;
    border-radius: 4px;
    background: var(--gold-wash);
    border: 1px solid rgba(156, 47, 102, .3);
    box-shadow: inset 0 0 0 3px var(--gold-wash), inset 0 0 0 4px rgba(156, 47, 102, .16);
    overflow: hidden;
}
.record-value {
    font-family: var(--font-display);
    font-size: clamp(1.85rem, 1.2rem + 2.6vw, 3rem);
    font-weight: 600;
    letter-spacing: -.035em;
    line-height: 1;
    color: var(--gold);
    font-variant-numeric: tabular-nums;
}

.prose { max-width: 68ch; }
.prose p + p { margin-top: 1rem; }
.prose h2, .prose h3 { margin-top: 2rem; margin-bottom: .75rem; }
.prose a { color: var(--sea); text-decoration: underline; text-underline-offset: 3px; }

/* The 2rem above a heading separates one topic from the next. It must not
   also apply to the heading that OPENS a card, or every panel is padded twice
   at the top and once at the bottom, and a column of them sits crooked. */
.prose > :first-child,
.prose .panel > :first-child,
.prose .callout > :first-child { margin-top: 0; }

/* A button is not a link in a sentence. `.prose a` underlines and recolours
   everything anchor-shaped, which turned the "Delete my account" control on
   the privacy policy into an underlined pill. */
.prose a.btn { text-decoration: none; color: inherit; }
.prose a.btn:hover { color: var(--sea); }

/* LISTS GET THEIR MARKERS BACK, AND ONLY HERE.
   The global reset is `ul, ol { list-style: none; padding: 0 }`, which is
   right for the navigation, the chip rows and the twenty other lists on this
   site that are lists structurally and something else visually. It is wrong
   inside prose, where a list is a list: the privacy policy's "what is
   collected" set as five unpunctuated paragraphs with nothing to show where
   one item ended and the next began. */
.prose ul, .prose ol { margin-block: 1rem; padding-left: 1.35rem; }
/* margin-block, not margin-top: the paragraph AFTER a list needs the gap
   as much as the one before it, and `.prose p + p` cannot give it — the
   list is sitting between them. */
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li + li { margin-top: .45rem; }

/* A CALLOUT — a panel that is making a point.
   Used in two views and declared in none, which is exactly the failure
   .stack-sm had: the account-deletion route on the privacy policy and the
   "nothing logged yet" line on the statistics page were both rendering as
   bare text on the page background, with no box around either. The ground is
   --sea-wash, the same informational tint .flash-info uses, so it reads as a
   notice whether it sits on a card or straight on the page. */
.callout {
    background: var(--sea-wash);
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: clamp(1.05rem, 2.2vw, 1.4rem) clamp(1.15rem, 2.5vw, 1.55rem);
}
.callout > * + * { margin-top: .75rem; }
.callout h3 { font-size: var(--t-md); }
.prose .callout { margin-top: 1.5rem; }

/* --------------------------------------------------------------------------
   25. Footer
   -------------------------------------------------------------------------- */
.footer {
    margin-top: clamp(3rem, 8vw, 6rem);
    padding-block: clamp(2.5rem, 5vw, 4rem) 2rem;
    border-top: 1px solid var(--line-strong);
    background: var(--paper-warm);
}
.footer-grid { display: grid; gap: var(--gap-lg); grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); }
.footer h4 {
    font-size: var(--t-sm); text-transform: uppercase; letter-spacing: .13em;
    color: var(--ink-mute); font-family: var(--font-body); font-weight: 700; margin-bottom: .9rem;
}
.footer ul { display: grid; gap: .5rem; }
.footer a { font-size: var(--t-sm); color: var(--ink-soft); }
.footer a:hover { color: var(--sea); }
.footer-base {
    margin-top: 2.5rem; padding-top: 1.5rem;
    border-top: 1px solid var(--line);
    font-size: var(--t-xs); color: var(--ink-mute);
    display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
}

/* --------------------------------------------------------------------------
   26. Utilities
   -------------------------------------------------------------------------- */
.sr-only {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.hidden { display: none !important; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: .5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2rem; }
.mb-1 { margin-bottom: .5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.flex-1 { flex: 1; }
.gap-sm { gap: .5rem; }
.w-full { width: 100%; }
.nowrap { white-space: nowrap; }
.row-center { justify-content: center; }

[data-reveal] {
    opacity: 0;
    transform: translateY(20px);
    transition: opacity .75s var(--ease), transform .75s var(--ease);
}
[data-reveal].in { opacity: 1; transform: none; }

/* --------------------------------------------------------------------------
   27. Preferences
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
        scroll-behavior: auto !important;
    }
    .rays, .bubbles { display: none; }
    /* The reef stays — it is the scene, not an effect. Only motion stops:
       fish park where they are, corals stop swaying, bubbles go. */
    .aq-sway, .aq-rays span { animation: none; }
    .aq-fish { animation-play-state: paused; }
    .aq-bubbles { display: none; }
    [data-reveal] { opacity: 1; transform: none; }
}

@media (prefers-contrast: more) {
    :root {
        --ink-mute: #4a6268;
        --ink-faint: #5f767c;
        --line: rgba(12, 31, 36,.22);
        --line-strong: rgba(12, 31, 36,.4);
    }
    .water::after { opacity: .12; }
}

@media print {
    .water, .aq, .bubbles, .rays, .masthead, .footer, .btn { display: none !important; }
    body { background: #fff; }
    .panel, .card { box-shadow: none; border: 1px solid #ccc; }
    .size-table tr { break-inside: avoid; }
    /* Charts are inline SVG, so they print. That is most of the reason they
       are inline SVG. */
    .chart { break-inside: avoid; }
    .lightbox { display: none !important; }
}

/* ==========================================================================
   28. RANKING, TACKLE, CHARTERS, PHOTOS, CHARTS
   ==========================================================================
   Everything added after the first release. Kept in one block at the end
   rather than threaded through the file above, so the diff is readable and
   nothing already working gets nudged.
   ========================================================================== */

/* --------------------------------------------------------------------------
   28.1 Charts

   Charts are sized by CSS and never by the SVG, which carries only a
   viewBox. One rule here therefore controls every diagram on the site, and
   none of them can overflow their column on a phone.
   -------------------------------------------------------------------------- */
.chart {
    display: block;
    width: 100%;
    height: auto;
    max-width: 100%;
    overflow: visible;
}
.chart text { font-family: var(--font-ui, inherit); }

/* A panel whose only content is a bar chart.
   The chart is drawn at a fixed unit width and scaled by its container, so a
   full-bleed panel does not give it more chart — it gives it bigger type. This
   caps the panel near the chart's own width on a wide screen and lets it go
   full width on a phone, where the constraint is the screen rather than the
   layout. */
.board-panel { max-width: 32rem; }
.chart-gauge, .chart-donut { max-width: 168px; margin-inline: auto; }
.chart-radar { max-width: 260px; margin-inline: auto; }
.chart-depth { max-width: 150px; }
.chart-spark { max-width: 100%; }

.legend-inline {
    display: flex; flex-wrap: wrap; gap: .35rem .85rem;
    font-size: var(--t-xs, .78rem); color: var(--ink-mute);
}
.legend-inline span { display: inline-flex; align-items: center; gap: .3rem; }
.legend-inline i { width: 9px; height: 9px; border-radius: 2px; flex: none; }

/* --------------------------------------------------------------------------
   28.2 Ranking Points
   -------------------------------------------------------------------------- */
.points-panel { padding: 1rem 1.2rem; }
.points-value {
    font-size: clamp(1.5rem, 4vw, 2rem);
    font-weight: 700;
    line-height: 1;
    color: var(--points-green);
    letter-spacing: -.02em;
}
.points-details > summary {
    cursor: pointer;
    font-size: var(--t-xs, .78rem);
    color: var(--sea);
    list-style: none;
}
.points-details > summary::-webkit-details-marker { display: none; }
.points-details > summary::before { content: "▸ "; }
.points-details[open] > summary::before { content: "▾ "; }

/* --------------------------------------------------------------------------
   28.3 Ranks and medals

   Rank colours run the reef spectrum from cool to hot, so the ladder reads
   as a progression at a glance without anyone needing to know the names.
   -------------------------------------------------------------------------- */
.rank-panel { padding: 1.1rem 1.3rem; }

/* ── THE RANK CARD ────────────────────────────────────────────────────────
   Emblem, standing, ladder, next goal — in that order, because that is the
   order the questions come in: who am I, where am I, what is next.

   The old card put the position off on the right of the title row and the
   two goal lines inside the ladder SVG, which meant three separate baselines
   fighting for the same glance. One column, one rhythm. */
.rank-card { padding: clamp(1.15rem, 3vw, 1.6rem); }

.rank-card-head {
    display: flex;
    align-items: center;
    gap: clamp(.9rem, 2.5vw, 1.25rem);
}
.rank-card-id { min-width: 0; }

.rank-card-meta {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .4rem;
    margin: .3rem 0 0;
}
.rank-card-sep { color: var(--ink-faint); }

.rank-ladder { margin-top: 1.15rem; }

.rank-ladder-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: .45rem;
}

/* THE BAND YOU ARE IN, in its own metal. This is the label the ten segments
   below need in order to mean anything — a gold bar with no word "Gold" next
   to it is a colour, not a rank. */
.rank-band {
    font-size: var(--t-xs);
    font-weight: 800;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--band, var(--ink-mute));
}
.rank-band-next {
    font-size: var(--t-xs);
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

/* THE TWO GOALS. The near one is the next grade — reachable this week. The
   far one is the next material, which is the milestone anybody would actually
   mention out loud. Printing only the near one, as this card used to, made
   the ladder look like it was made of half-steps. */
.rank-next {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .25rem 1rem;
    margin: .9rem 0 0;
}
.rank-next-near { font-size: var(--t-sm); color: var(--ink-soft); }
.rank-next-far  { font-size: var(--t-sm); color: var(--ink-faint); }

@media (max-width: 560px) {
    /* The emblem gives up its row rather than squeezing the title to two
       words per line. */
    .rank-card-head { align-items: flex-start; }
    .rank-next { flex-direction: column; gap: .15rem; }
}
.rank-title {
    font-family: var(--font-display, inherit);
    font-size: clamp(1.15rem, 3vw, 1.6rem);
    line-height: 1.1;
}
.rank-tag { font-weight: 600; }
.rank-dot {
    display: inline-block; width: 9px; height: 9px;
    border-radius: 50%; margin-right: .4rem; vertical-align: middle;
}

/* Twenty ranks, climbing cyan → green → blue → gold → orange → coral →
   violet. The colour has to carry the hierarchy on its own: nobody memorises
   twenty titles, but everybody can see that violet outranks cyan. */
.rank-deckhand        { color: var(--ink-mute); }
.rank-bait_runner     { color: var(--h3); }
.rank-angler          { color: var(--h3); }
.rank-rod_hand        { color: var(--h3); }
.rank-shore_master    { color: var(--h4); }
.rank-inshore_hunter  { color: var(--h4); }
.rank-specimen_hunter { color: var(--h4); }
.rank-reef_runner     { color: var(--h2); }
.rank-sport_angler    { color: var(--h2); }
.rank-wreck_master    { color: var(--h2); }
.rank-trophy_hunter   { color: var(--h5); }
.rank-master_angler   { color: var(--h5); }
.rank-offshore_master { color: var(--h5); }
.rank-big_game        { color: var(--h6); }
.rank-blue_water      { color: var(--h6); }
.rank-deep_water      { color: var(--h6); }
.rank-grand_slam      { color: var(--h7); }
.rank-record_chaser   { color: var(--h7); }
.rank-legend          { color: var(--h1); font-weight: 700; }
.rank-leviathan       { color: var(--h1); font-weight: 700; letter-spacing: .01em; }

/* The grade numeral. Set slightly smaller and lighter than the rank name, so
   "Master Angler II" reads as one title with a suffix rather than as two
   competing pieces of information. */
.rank-grade {
    display: inline-block;
    margin-left: .3em;
    font-size: .78em;
    font-weight: 600;
    opacity: .75;
    letter-spacing: .06em;
    vertical-align: baseline;
}

/* The three grade pips shown against each rank on the ladder page. */
.grade-row { display: inline-flex; gap: .2rem; }
.grade-pip {
    min-width: 1.5rem;
    padding: .05rem .3rem;
    text-align: center;
    border-radius: 3px;
    font-size: var(--t-xs, .7rem);
    font-weight: 600;
    color: var(--ink-faint);
    background: var(--surface);
}
.grade-pip.on {
    color: var(--card);
    background: var(--sea);
}

.medal {
    display: inline-flex; align-items: center; gap: .3rem;
    font-size: var(--t-xs, .78rem); font-weight: 600;
    padding: .12rem .5rem; border-radius: 999px;
    border: 1px solid currentColor;
}
.medal-bronze   { color: #a06a3c; }
.medal-silver   { color: #7d8b93; }
.medal-gold     { color: var(--mark); }
.medal-platinum { color: var(--h1); }
[data-theme="harbor"] .medal-bronze { color: #cf9862; }
[data-theme="harbor"] .medal-silver { color: #b3c0c7; }

.you-are {
    font-size: var(--t-sm, .9rem);
    color: var(--ink-soft);
    padding: .5rem .8rem;
    border-left: 3px solid var(--sea);
    background: var(--surface);
    border-radius: 0 var(--r-sm, 5px) var(--r-sm, 5px) 0;
}

.example-card {
    padding: .9rem 1rem;
    border: 1px solid var(--line);
    border-radius: var(--r-md, 9px);
    background: var(--surface);
}

/* --------------------------------------------------------------------------
   28.4 Chips (extensions)
   -------------------------------------------------------------------------- */
.chip-row { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.chip-sm  { font-size: var(--t-xs, .74rem); padding: .1rem .45rem; }

/* A chip for something with a deadline on it — currently the admin list's
   "deleting in 30 days". Warm rather than red: it is a countdown somebody
   chose to start, not a fault. */
.chip-warn {
    color: #a8620c;
    border-color: rgba(200, 120, 20, .45);
    background: rgba(200, 120, 20, .10);
}
[data-theme="harbor"] .chip-warn { color: #f0bd6a; }

/* Chips that sit together in a narrow cell.
   Inline chips separated by a text space look fine on one line and then touch
   the moment the column narrows and they wrap, because a text space has no
   height. A wrapping flex box with an explicit row-gap spaces both axes. */
.chip-stack {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem .35rem;
    align-items: center;
}

/* Belt and braces for anywhere chips are still emitted as loose inline
   elements — a hairline of vertical margin so a wrap can never collide. */
.index td .chip,
.data td .chip { margin-block: 1px; }
.chip-on  { border-color: var(--sea); color: var(--sea); background: var(--sea-wash); }
button.chip { cursor: pointer; font: inherit; }
button.chip:hover { border-color: var(--sea); color: var(--sea); }

.panel-label {
    font-size: var(--t-xs, .72rem);
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ink-mute);
    margin: 0 0 .35rem;
}

.stat-row { display: flex; flex-wrap: wrap; gap: 1.2rem; }
.stat-row .stat b { display: block; font-size: var(--t-md, 1.1rem); line-height: 1.1; }
.stat-row .stat span { font-size: var(--t-xs, .74rem); color: var(--ink-mute); }

.panel-quiet { background: var(--surface); border-style: dashed; }
.note-quiet {
    font-size: var(--t-sm, .88rem); color: var(--ink-mute);
    padding: .6rem .8rem; border: 1px dashed var(--line-strong);
    border-radius: var(--r-sm, 5px);
}
.plain-list { list-style: none; padding: 0; margin: 0; display: grid; gap: .5rem; }

.row-title { font-weight: 600; }

/* --------------------------------------------------------------------------
   28.5 Month strips

   Twelve cells instead of a sentence. A season is a shape, and a shape is
   read faster than "May to October, peaking July–August".
   -------------------------------------------------------------------------- */
.month-strip { display: inline-flex; gap: 1.5px; }
.month-strip i {
    width: 7px; height: 15px; border-radius: 1.5px;
    background: var(--surface-raised);
}
.month-strip i.on   { background: var(--h4); opacity: .55; }
.month-strip i.peak { background: var(--h7); opacity: 1; }

/* --------------------------------------------------------------------------
   28.6 Photographs
   -------------------------------------------------------------------------- */
.catch-gallery {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: .4rem;
}
.catch-shot {
    display: block; overflow: hidden; position: relative;
    border-radius: var(--r-sm, 6px);
    aspect-ratio: 1 / 1;
    border: 1px solid var(--line);
}
.catch-shot-lead { grid-column: 1 / -1; aspect-ratio: 4 / 3; border-radius: var(--r-lg, 12px); }
.catch-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.catch-shot:hover img { transform: scale(1.02); }
.catch-shot img { transition: transform .5s var(--ease, ease); }

/* A clip in a feed post or a catch gallery fills its slot the way a
   photograph does. object-fit matters: without it a portrait clip letterboxes
   inside a landscape slide and the post grows a black frame. */
.post-slide-video, .catch-shot-video { display: block; overflow: hidden; }
.post-slide-video video, .catch-shot-video video {
    width: 100%; height: 100%; object-fit: cover; display: block;
    background: #000;
}
.catch-shot-video video { border-radius: inherit; }

/* The map's live readout. Sits above the status line, inside the map shell,
   and is hidden entirely when nothing is in view — an empty row of dashes is
   noise, not information. */
.charter-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 1.6rem;
    margin: 0 0 1.4rem;
    padding: .9rem 1.1rem;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-md, 10px);
}

.map-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 1.1rem;
    padding: .55rem .8rem;
    background: var(--card);
    border-top: 1px solid var(--line);
}
.map-stat b {
    display: block;
    font-size: var(--t-md, 1.05rem);
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
    color: var(--sea-deep, var(--sea));
}
.map-stat span {
    font-size: 10.5px;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--ink-mute, #8a9ca8);
}

/* --- Statistics page -----------------------------------------------------
   Bars are CSS, not canvas. The height and width land from --h / --w, which
   the server rendered, so the chart is correct before any script runs; the
   JavaScript only replays it from zero as a transition. Under
   prefers-reduced-motion the transition is dropped and the bars are simply
   already the right size. */
.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
    border-radius: var(--r-md, 10px);
    overflow: hidden;
}

.chart-scroll { overflow-x: auto; padding-bottom: .3rem; }

.bar-chart {
    display: flex;
    align-items: flex-end;
    gap: .5rem;
    min-width: 460px;
    height: 190px;
    padding-top: .5rem;
}
.bar-col { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: .25rem; }
.bar-track {
    width: 100%;
    flex: 1;
    display: flex;
    align-items: flex-end;
    background: linear-gradient(var(--line) 1px, transparent 1px) 0 0 / 100% 25%;
}
.bar-fill {
    width: 100%;
    height: var(--h, 0%);
    min-height: 2px;
    border-radius: 4px 4px 0 0;
    background: linear-gradient(180deg, var(--sea-bright, #1a8cc0), var(--sea, #0a6e9e));
    transition: height .7s cubic-bezier(.22, 1, .36, 1);
}
.bar-n { font-size: 11px; font-weight: 600; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.bar-label { font-size: 10.5px; color: var(--ink-mute, #8a9ca8); text-transform: uppercase; letter-spacing: .03em; }

.rank-bars { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.rank-bars li { display: grid; grid-template-columns: minmax(90px, 22%) 1fr auto; align-items: center; gap: .7rem; }
.rank-bar-name { font-size: var(--t-sm, .9rem); text-decoration: none; color: var(--ink); }
.rank-bar-name:hover { color: var(--sea); }
.rank-bar-track { background: var(--surface-sunk, rgba(0,0,0,.05)); border-radius: 999px; height: 12px; overflow: hidden; }
.rank-bar-fill {
    height: 100%;
    width: var(--w, 0%);
    min-width: 3px;
    border-radius: 999px;
    background: var(--tint, var(--sea));
    transition: width .8s cubic-bezier(.22, 1, .36, 1);
}
.rank-bar-n { font-size: var(--t-sm, .9rem); font-weight: 600; font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
    .bar-fill, .rank-bar-fill { transition: none; }
}

/* The irreversible corner of account settings.
   Bordered and tinted rather than tucked away, because hiding a destructive
   control behind a disclosure does not make it safer — it makes it a surprise
   for whoever finds it by accident. */
.danger-zone {
    border: 1px solid rgba(200, 60, 70, .45);
    border-radius: var(--r-md, 10px);
    padding: 1.1rem 1.2rem 1.3rem;
    background: rgba(200, 60, 70, .05);
}
.danger-zone h2 { margin: 0 0 .35rem; font-size: var(--t-lg, 1.1rem); color: #c83c46; }

/* The irreversible button gets more room than the stack gives it, and a rule
   above it. Both gates sit directly over this control, and a destructive
   button one line below the field you just typed into is a button you can
   reach by pressing Tab-then-Enter without ever looking down. The gap is the
   pause. */
/* The recovery window. Not muted, because it is the one sentence in this box
   that is good news, and the one people scan straight past. */
.danger-zone .delete-window {
    color: var(--ink-soft);
    border-left: 3px solid rgba(200, 60, 70, .35);
    padding-left: .7rem;
}

.danger-zone .row-end {
    margin-top: 1.5rem;
    padding-top: 1.15rem;
    border-top: 1px solid rgba(200, 60, 70, .22);
}

/* --- Camera capture ------------------------------------------------------
   The two capture controls are <label>s wrapping a hidden file input, which
   is what makes them keyboard-reachable and screen-reader-correct for free:
   the label is the control, so Tab and Enter already work and there is no
   click handler to write. Do not swap them for buttons that .click() an
   input — that loses both. */
.capture-row { display: flex; flex-wrap: wrap; gap: .5rem; }
.capture-btn { cursor: pointer; display: inline-flex; align-items: center; gap: .4rem; }
.capture-btn svg {
    width: 17px; height: 17px; flex: none;
    fill: none; stroke: currentColor; stroke-width: 1.7;
    stroke-linecap: round; stroke-linejoin: round;
}
/* :focus-within, because the focus lands on the input this label hides. */
.capture-btn:focus-within { outline: 2px solid var(--sea); outline-offset: 2px; }

/* A clip in the preview strip: same tile as a photograph, with a play badge
   so it is not mistaken for a still that failed to load. */
.photo-strip video {
    width: 72px; height: 72px; object-fit: cover;
    border-radius: var(--r-sm, 6px); border: 1px solid var(--line);
    display: block; background: var(--surface);
}
.photo-tile { position: relative; display: inline-block; }
.photo-tile .play-badge {
    position: absolute; inset: 0; display: grid; place-items: center;
    pointer-events: none;
}
.photo-tile .play-badge::after {
    content: ''; width: 0; height: 0;
    border-left: 13px solid rgba(255, 255, 255, .95);
    border-top: 8px solid transparent; border-bottom: 8px solid transparent;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .5));
    margin-left: 3px;
}

.photo-strip { display: flex; flex-wrap: wrap; gap: .4rem; }
.photo-strip img, .photo-thumb {
    width: 72px; height: 72px; object-fit: cover;
    border-radius: var(--r-sm, 6px); border: 1px solid var(--line);
    display: block; position: relative; overflow: hidden;
}
.photo-thumb img { width: 100%; height: 100%; border: 0; border-radius: 0; }
.photo-flag {
    position: absolute; inset: auto 0 0 0;
    font-size: 9px; text-align: center;
    background: rgba(var(--shadow-rgb), .6); color: #fff;
    padding: 1px 0;
}

.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: .5rem;
}
.photo-cell {
    position: relative; display: block; overflow: hidden;
    aspect-ratio: 4 / 3;
    border-radius: var(--r-md, 9px); border: 1px solid var(--line);
}
.photo-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-caption {
    position: absolute; inset: auto 0 0 0;
    padding: .8rem .5rem .35rem;
    font-size: var(--t-xs, .72rem); font-weight: 600; color: #fff;
    background: linear-gradient(to top, rgba(0,0,0,.62), transparent);
}
.photo-caption em { display: block; font-style: normal; font-weight: 400; opacity: .8; }

.inline-upload { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.input-sm { padding: .3rem .5rem; font-size: var(--t-xs, .8rem); }

/* Lightbox --------------------------------------------------------------- */
.lightbox {
    position: fixed; inset: 0; z-index: 2000;
    display: none;
    align-items: center; justify-content: center;
    background: rgba(0, 0, 0, .88);
    padding: 2rem 3.5rem;
}
.lightbox[data-open="true"] { display: flex; }
.lightbox-stage { margin: 0; max-width: 100%; max-height: 100%; text-align: center; }
.lightbox-stage img {
    max-width: 100%; max-height: 82vh;
    border-radius: var(--r-md, 9px);
    display: block; margin: 0 auto;
}
.lightbox-stage figcaption {
    color: rgba(255, 255, 255, .8);
    font-size: var(--t-sm, .88rem);
    margin-top: .7rem;
}
.lightbox-close, .lightbox-nav {
    position: absolute; background: none; border: 0;
    color: rgba(255, 255, 255, .75); cursor: pointer;
    line-height: 1; padding: .5rem;
}
.lightbox-close { top: 1rem; right: 1.2rem; font-size: 2rem; }
.lightbox-nav { top: 50%; transform: translateY(-50%); font-size: 3rem; }
.lightbox-prev { left: .4rem; }
.lightbox-next { right: .4rem; }
.lightbox-close:hover, .lightbox-nav:hover { color: #fff; }

@media (max-width: 640px) {
    .lightbox { padding: 1rem; }
    .lightbox-nav { font-size: 2rem; }
}

/* --------------------------------------------------------------------------
   28.7 Map additions
   -------------------------------------------------------------------------- */
/* Three named sizes instead of an inline height per call site. Every embedded
   map on the site was carrying its own literal — 280px here, 380 there, 440
   somewhere else — which is how you end up with six maps that are all slightly
   different for no reason anyone can reconstruct.
     sm    a location fix on a single catch: it only has to show where
     md    a page where the map is one panel among several
     tall  the map page, where the map IS the page */
.map-shell-sm   { height: clamp(340px, 44vh, 460px); }
.map-shell-md   { height: clamp(460px, 58vh, 620px); }
.map-shell-tall { height: clamp(520px, 80vh, 900px); }

/* The catch form's picker: what the pin is on, and the one control it adds. */
.pick-status {
    display: flex; align-items: center; justify-content: space-between; gap: .75rem;
    min-height: 2.25rem; margin-top: .5rem; font-size: .9rem; color: var(--ink-soft);
}
.pick-status-text { overflow-wrap: anywhere; }
.map-pick-base {
    font: 600 .8rem/1 inherit; padding: .55rem .8rem; border-radius: 8px; cursor: pointer;
    background: var(--card); color: var(--ink-soft); border: 1px solid var(--line-strong);
    box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
}

.seg {
    display: inline-flex; flex-wrap: wrap;
    border: 1px solid var(--line); border-radius: var(--r-sm, 6px);
    overflow: hidden;
}
.seg-btn {
    background: none; border: 0; cursor: pointer;
    font: inherit; font-size: var(--t-xs, .74rem);
    padding: .3rem .55rem; color: var(--ink-mute);
    border-right: 1px solid var(--line);
}
.seg-btn:last-child { border-right: 0; }
.seg-btn:hover { color: var(--ink); background: var(--surface); }
.seg-btn[aria-pressed="true"] { background: var(--sea); color: #fff; }

.map-top {
    /* Beside the zoom buttons, not under them: they sit in the same corner
       and used to cover the right-hand column of this list. */
    position: absolute; right: 3.6rem; top: .75rem; z-index: 500;
    width: min(240px, 42vw);
    background: var(--card); border: 1px solid var(--line);
    border-radius: var(--r-md, 9px);
    padding: .6rem .7rem;
    box-shadow: 0 6px 22px rgba(var(--shadow-rgb), .12);
    max-height: 46%; overflow: auto;
}
.top-row {
    display: grid; grid-template-columns: 1.4rem 1fr auto;
    gap: .4rem; align-items: baseline;
    padding: .28rem 0; border-bottom: 1px solid var(--line);
    font-size: var(--t-xs, .76rem);
}
.top-row:last-child { border-bottom: 0; }
.top-rank { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.top-name em { display: block; font-style: normal; color: var(--ink-faint); font-size: .92em; }
.top-metric { font-weight: 600; color: var(--h7); }

.legend-shapes { margin-top: .45rem; border-top: 1px solid var(--line); padding-top: .45rem; }

/* Leaflet popups, restyled to match the site rather than the library ------ */
.leaflet-popup-content-wrapper {
    background: var(--card); color: var(--ink);
    border-radius: var(--r-md, 9px);
    box-shadow: 0 8px 28px rgba(var(--shadow-rgb), .2);
}
.leaflet-popup-tip { background: var(--card); }
.leaflet-popup-content { margin: .7rem .85rem; line-height: 1.45; }

.pop-photo {
    width: calc(100% + 1.7rem); margin: -.7rem -.85rem .55rem;
    height: 110px; object-fit: cover; display: block;
    border-radius: var(--r-md, 9px) var(--r-md, 9px) 0 0;
}
/* The catch card. Media edge to edge, then the facts. */
.fr-popup .leaflet-popup-content-wrapper { padding: 0; overflow: hidden; border-radius: 14px; }
.fr-popup .leaflet-popup-content { margin: 0 !important; width: auto !important; min-width: 230px; }
.fr-popup.fr-popup-plain .leaflet-popup-content { margin: .8rem .95rem !important; }
.fr-popup .pop-photo { width: 100%; margin: 0; height: 128px; border-radius: 0; }
.pop-art {
    height: 96px; display: grid; place-items: center; padding: .5rem 1rem;
    background:
        radial-gradient(120% 90% at 50% 0%, rgba(255, 255, 255, .9), transparent 70%),
        linear-gradient(180deg, color-mix(in srgb, var(--acc) 16%, #eef4f7), color-mix(in srgb, var(--acc) 30%, #dce8ef));
    border-bottom: 1px solid var(--line);
}
.pop-art img { max-width: 100%; max-height: 84px; }
.pop-body { padding: .7rem .9rem .8rem; }
.pop-head { display: flex; align-items: center; justify-content: space-between; gap: .6rem; }
.pop-score {
    flex: none; font-size: .74rem; font-weight: 800; color: #fff; padding: .14rem .5rem; border-radius: 999px;
    background: var(--pin); font-variant-numeric: tabular-nums;
}
.fr-popup .pop-link {
    display: block; margin-top: .6rem; text-align: center; padding: .45rem .6rem; border-radius: 9px;
    background: var(--sea-wash, #e4eff6); font-weight: 600;
}
.fr-popup.fr-popup-plain .pop-link { display: inline-block; background: none; padding: 0; text-align: left; }
.fr-popup .leaflet-popup-close-button { color: #fff !important; text-shadow: 0 1px 3px rgba(0, 0, 0, .5); top: 4px !important; right: 4px !important; }
.fr-popup.fr-popup-plain .leaflet-popup-close-button { color: var(--ink-mute) !important; text-shadow: none; }
.pop-title { font-size: .95rem; display: block; }
.pop-row { display: flex; gap: .5rem; align-items: baseline; margin-top: .15rem; font-size: .84rem; }
.pop-points { color: var(--h7); font-weight: 600; }
.pop-meta { font-size: .76rem; color: var(--ink-mute); margin-top: .1rem; }
.pop-tag {
    font-size: .68rem; border: 1px solid var(--h4); color: var(--h4);
    border-radius: 999px; padding: 0 .35rem;
}
.pop-link { display: inline-block; margin-top: .4rem; font-size: .8rem; color: var(--sea); }
.pop-note { font-size: .7rem; color: var(--ink-faint); margin-top: .35rem; }

@media (max-width: 720px) {
    /* On a phone the panels would cover the map they describe. */
    .map-top { display: none; }
}

.map-panel-toggle { display: none; }
@media (max-width: 780px) {
    .map-panel-toggle {
        display: inline-flex; align-items: center; gap: .45rem;
        position: absolute; top: 1rem; left: 1rem; z-index: 600;
        padding: .55rem .9rem; border-radius: 999px; font-weight: 600; font-size: .85rem;
        background: var(--card); color: var(--ink); border: 1px solid var(--line-strong); box-shadow: var(--shadow-sm);
    }
    .map-panel-toggle svg { width: 16px; height: 16px; }
    .map-panel { display: none; top: 3.8rem; max-height: calc(100% - 5rem); overflow: auto; }
    .map-shell.panel-open .map-panel { display: block; }
    .map-status { top: auto; bottom: 1rem; }
}

/* --------------------------------------------------------------------------
   28.8 Form additions
   -------------------------------------------------------------------------- */
.check-lead { align-items: flex-start; }
.check-lead b { display: block; }
.check-lead span span { display: block; }

/* --------------------------------------------------------------------------
   29. BUILD BARS, FEED, WELCOME
   -------------------------------------------------------------------------- */

/* ---- 29.1 Build bars ----------------------------------------------------
   Two bars on one shared scale: length above, record weight below. Where they
   diverge you are reading the fish's proportions, which is the one thing the
   thumbnail beside them cannot tell you. */
.build { display: grid; gap: 3px; min-width: 90px; }
.build-track {
    position: relative;
    height: 7px;
    border-radius: 4px;
    background: var(--surface);
    overflow: visible;
}
.build-bar { display: block; height: 100%; border-radius: 4px; }
.build-len { background: var(--h3); }
.build-wt  { background: var(--h5); opacity: .85; }

/* The 1.8 m mark. A person, so "bigger than me" needs no arithmetic. */
.build-human {
    position: absolute;
    top: -2px; bottom: -2px;
    width: 0;
    border-left: 1.5px dashed var(--ink-faint);
    opacity: .75;
}

/* ---- 29.2 Feed ---------------------------------------------------------
   A single wide column. Everywhere else on this site is a dense table, which
   is right for comparing 257 species and wrong for looking at one fish. */
.feed-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap-lg, 2rem);
    justify-content: center;
}

/* THE TWO-COLUMN GRID ONLY APPLIES WHEN A RAIL IS ACTUALLY THERE.
   It used to apply unconditionally while the rail rendered only on an empty
   feed — so the moment you had posts, a 280px column stayed reserved and
   empty and the whole feed sat 140px left of centre. That was the "too far
   left". The modifier class makes the dependency explicit instead of leaving
   the layout to guess. */
@media (min-width: 1040px) {
    .feed-layout--rail {
        grid-template-columns: minmax(0, 560px) 300px;
        justify-content: center;
    }
}
.feed-main  { min-width: 0; max-width: 560px; margin-inline: auto; width: 100%; }
.feed-title { margin-bottom: 1.2rem; }

.feed-rail {
    align-self: start;
    position: sticky;
    top: 88px;
    display: grid;
    gap: 1rem;
}
/* Below the two-column breakpoint the rail would be a second full-width
   column of chrome under the feed, which is worse than not having it. */
@media (max-width: 1039px) { .feed-rail { display: none; } }

.rail-card { padding: .95rem 1.05rem; }
.rail-points {
    margin: .25rem 0 0;
    font-size: var(--t-lg, 1.3rem);
    font-weight: 700;
    line-height: 1;
    color: var(--h7);
}
.rail-points em { font-style: normal; font-size: .55em; font-weight: 400; opacity: .7; }
.rail-progress {
    margin-top: .7rem;
    height: 6px;
    border-radius: 3px;
    background: var(--surface-raised);
    overflow: hidden;
}
.rail-progress span { display: block; height: 100%; border-radius: 3px; background: var(--sea); }
.rail-stats { display: flex; gap: 1rem; flex-wrap: wrap; }
.rail-stats span { display: grid; }
.rail-stats b { font-size: var(--t-md, 1.05rem); line-height: 1.1; }
.rail-stats span > *:last-child,
.rail-stats span { font-size: var(--t-xs, .72rem); color: var(--ink-mute); }

.feed { display: grid; gap: 1.6rem; }

.post {
    border: 1px solid var(--line);
    border-radius: var(--r-lg, 12px);
    background: var(--card);
    overflow: hidden;
}

/* ---- head ---- */
.post-head { display: flex; align-items: center; gap: .6rem; padding: .7rem .85rem; }
.post-who  { min-width: 0; flex: 1; }
.post-angler { font-weight: 600; font-size: var(--t-sm, .92rem); }
.post-place {
    display: block;
    font-size: var(--t-xs, .74rem);
    color: var(--ink-faint);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.post-time { font-size: var(--t-xs, .74rem); color: var(--ink-faint); white-space: nowrap; }

/* ---- media ----
   Square, because a feed of mixed aspect ratios reads as a jumble. The photo
   is object-fit cover; the generated illustration gets to breathe instead. */
.post-media { position: relative; background: var(--tint, var(--surface)); }
.post-single, .post-slide { display: block; }
.post-single img, .post-slide img {
    display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
}
.post-art {
    display: grid; place-items: center;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    aspect-ratio: 4 / 3; padding: 1.5rem;
    background: linear-gradient(180deg, var(--w-050), var(--w-200));
}
/* Both axes, for the reason spelled out on .species-media-art svg: a deep
   fish is taller than this 4/3 tile and was running out of the bottom of the
   card. The explicit row gives the percentage something definite to measure. */
.post-art svg { max-width: 100%; max-height: 100%; height: auto; }

/* Scroll-snap carousel — no slider library, and it works with a finger, a
   trackpad, the arrow keys and a scrollbar because it is just a scroll box. */
.post-strip {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
}
.post-strip::-webkit-scrollbar { display: none; }
.post-strip > * { flex: 0 0 100%; scroll-snap-align: center; }

.post-dots {
    position: absolute; left: 0; right: 0; bottom: .6rem;
    display: flex; justify-content: center; gap: .3rem;
}
.post-dots i {
    width: 6px; height: 6px; border-radius: 50%;
    background: rgba(255, 255, 255, .55);
    box-shadow: 0 1px 3px rgba(0, 0, 0, .4);
    transition: background .2s var(--ease, ease);
}
.post-dots i.on { background: #fff; }

/* Figures ride on the image — this is still a fishing site and how big it
   was is the first thing anyone wants to know. */
.post-figures {
    position: absolute; left: .7rem; bottom: .7rem;
    display: flex; align-items: baseline; gap: .5rem;
    padding: .3rem .6rem;
    border-radius: 999px;
    background: rgba(0, 0, 0, .55);
    backdrop-filter: blur(4px);
    color: #fff;
}
.post-weight { font-weight: 700; font-size: var(--t-sm, .95rem); }
.post-len    { font-size: var(--t-xs, .76rem); opacity: .85; }
.post-trophy { position: absolute; right: .7rem; top: .7rem; }

/* ---- actions ---- */
.post-actions {
    display: flex; align-items: center; gap: .3rem;
    padding: .5rem .6rem .1rem;
}
.icon-btn {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: .4rem .5rem;
    border: 0; background: none; cursor: pointer;
    color: var(--ink-mute);
    font: inherit; font-size: var(--t-xs, .8rem);
    border-radius: var(--r-sm, 6px);
}
.icon-btn svg {
    width: 22px; height: 22px;
    fill: none; stroke: currentColor; stroke-width: 1.7;
    stroke-linecap: round; stroke-linejoin: round;
}
.icon-btn:hover { color: var(--ink); background: var(--surface); }
.icon-btn.on { color: var(--clay); }
.icon-btn.on svg { fill: var(--clay); stroke: var(--clay); }
.icon-btn.is-static { cursor: default; }
.icon-btn.is-static:hover { background: none; color: var(--ink-mute); }
.post-like { display: contents; }

.post-points {
    margin-left: auto;
    font-weight: 700;
    color: var(--h7);
    font-size: var(--t-sm, .9rem);
    padding-right: .4rem;
}
.post-points em { font-style: normal; font-weight: 400; opacity: .7; font-size: .8em; }

/* ---- caption ---- */
.post-body { padding: .2rem .95rem 1rem; }
.post-caption { margin: 0; font-size: var(--t-sm, .92rem); line-height: 1.5; }
.post-species { font-weight: 600; }
.post-notes { color: var(--ink-soft); }
.post-tags { margin-top: .55rem; }

.rail-angler { display: flex; align-items: center; gap: .55rem; }

/* A tap target on a phone is a finger, not a mouse pointer. */
@media (max-width: 640px) {
    .icon-btn { padding: .55rem .6rem; }
    .post { border-radius: var(--r, 10px); }
}

/* ---- 29.3 First-visit picker -------------------------------------------- */
.welcome {
    position: fixed;
    inset: 0;
    z-index: 1500;
    display: grid;
    place-items: center;
    padding: 1.5rem;
    background: rgba(var(--shadow-rgb), .55);
    backdrop-filter: blur(3px);
}
.welcome[data-closing="true"] { display: none; }
.welcome-card {
    width: min(440px, 100%);
    padding: 1.6rem 1.7rem;
    border-radius: var(--r-lg, 12px);
    background: var(--card);
    border: 1px solid var(--line);
    box-shadow: 0 24px 60px rgba(var(--shadow-rgb), .3);
}
.welcome-card h2 { margin: .1rem 0 .3rem; font-size: var(--t-lg, 1.3rem); }
.welcome-group { margin-top: 1.1rem; }
.welcome-options { display: flex; flex-wrap: wrap; gap: .45rem; }
.welcome-option {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    padding: .5rem .8rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm, 6px);
    background: var(--card);
    color: var(--ink-soft);
    font: inherit;
    font-size: var(--t-sm, .9rem);
    cursor: pointer;
}
.welcome-option:hover { border-color: var(--sea); color: var(--ink); }
.welcome-option.on {
    border-color: var(--sea);
    background: var(--sea-wash);
    color: var(--sea);
    font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
    .welcome { backdrop-filter: none; }
}

/* ---- Feed in the navbar --------------------------------------------------
   Leads the nav for signed-in anglers, separated by a hairline so it reads as
   "your stuff" rather than as one more reference section competing with the
   catalogue for attention. */
.nav-feed { display: inline-flex; align-items: center; gap: .4rem; }
.nav-feed svg { width: 17px; height: 17px; opacity: .8; }
.nav-feed[aria-current="page"] svg { opacity: 1; }
.nav-sep {
    width: 1px; height: 18px;
    margin-inline: .35rem;
    background: var(--line-strong);
    flex: none;
}
@media (max-width: 920px) {
    /* In the drawer the nav is a vertical stack; a vertical rule between two
       stacked links is meaningless, so it becomes a horizontal one. */
    .nav-sep { width: auto; height: 1px; align-self: stretch; margin: .4rem 0; }
}

/* --------------------------------------------------------------------------
   30. SIZE EXPECTATION + RECORD BARS
   --------------------------------------------------------------------------
   Both of these replace "relative size" comparisons, which were the same idea
   applied in two places where it did not hold up. Comparing a perch to a
   marlin by length is not a meaningful axis, and a fish scaled beside a
   person answers a question nobody asks twice.
   -------------------------------------------------------------------------- */

/* ---- 30.1 How big does it get -------------------------------------------- */
.expect { display: grid; gap: .5rem; }
.expect-row {
    display: grid;
    grid-template-columns: minmax(88px, 8.5rem) 1fr auto;
    align-items: center;
    gap: .7rem;
}
.expect-label { font-size: var(--t-xs, .78rem); color: var(--ink-mute); }
.expect-track {
    position: relative;
    height: 14px;
    border-radius: 7px;
    background: var(--surface);
}
.expect-bar {
    display: block;
    height: 100%;
    border-radius: 7px;
    min-width: 3px;
}
/* The human reference, reduced from a drawn figure to one line — it was
   never worth half a panel. */
.expect-human {
    position: absolute;
    top: -3px; bottom: -3px;
    width: 0;
    border-left: 1.5px dashed var(--ink-faint);
    opacity: .8;
}
.expect-value {
    font-size: var(--t-sm, .88rem);
    font-weight: 600;
    white-space: nowrap;
}
.expect-note { margin: .3rem 0 0; }
.expect-human-key {
    display: inline-block;
    width: 0; height: 9px;
    border-left: 1.5px dashed var(--ink-faint);
    vertical-align: -1px;
    margin-right: .3rem;
}
@media (max-width: 520px) {
    .expect-row { grid-template-columns: minmax(72px, 6.5rem) 1fr auto; gap: .5rem; }
}

/* ---- 30.2 Trophy Score bar (catch log) ----------------------------------- */
.record-bar {
    position: relative;
    width: min(38vw, 300px);
    min-width: 120px;
    height: 12px;
    border-radius: 6px;
    background: var(--surface);
    overflow: hidden;
}
.record-bar-track {
    position: absolute;
    inset: 0;
    /* Quarter marks — 25/50/75% of a world record are the tier boundaries
       everywhere else on the site, so the bar is readable against them. */
    background: repeating-linear-gradient(90deg,
                transparent 0 calc(25% - 1px),
                rgba(var(--shadow-rgb), .12) calc(25% - 1px),
                rgba(var(--shadow-rgb), .12) 25%);
}
.record-bar-fill {
    position: relative;
    z-index: 1;
    display: block;
    height: 100%;
    border-radius: 6px;
    background: var(--h3);
}
/* THE BAR HAS TO READ AS A CLIMB, not as five unrelated chart series.
   
   It was painted from --h4..--h7, which are CATEGORICAL hues — picked to stay
   distinguishable side by side in a legend, not to run in an order. So a 30%
   fish was green, a 60% one olive and a 90% one rust, and nothing about that
   sequence said "further along". Half the range had no colour of its own at
   all: everything under 25% fell through to the generic --h3.
   
   These run cool to hot, which is the one progression every reader already
   knows, and each is a gradient rather than a flat fill so the bar has some
   depth at the 10px height it is actually drawn at. The record tier is the
   only one that is metal, because it is the only one that is not a score.

   .passport-fill joins them because the catch page draws the same quantity —
   a share of the world record — with that class. It only takes a tier when
   that is what it is showing; the species-passport bar on an angler page is a
   collection count, carries no tier, and keeps the neutral fill. */
.record-bar-fill.tier-logged,
.passport-fill.tier-logged {
    background: linear-gradient(90deg,
        color-mix(in srgb, var(--tier-logged) 78%, white), var(--tier-logged));
}
.record-bar-fill.tier-notable,
.passport-fill.tier-notable {
    background: linear-gradient(90deg,
        color-mix(in srgb, var(--tier-notable) 72%, white), var(--tier-notable));
}
.record-bar-fill.tier-trophy,
.passport-fill.tier-trophy {
    background: linear-gradient(90deg,
        color-mix(in srgb, var(--tier-trophy) 72%, white), var(--tier-trophy));
}
.record-bar-fill.tier-legendary,
.passport-fill.tier-legendary {
    background: linear-gradient(90deg,
        color-mix(in srgb, var(--tier-legendary) 76%, white), var(--tier-legendary));
}
.record-bar-fill.tier-record,
.passport-fill.tier-record {
    background: linear-gradient(90deg, #d4a017, #f0c945 45%, #b8860b);
    box-shadow: 0 0 0 1px rgba(184, 134, 11, .5) inset;
}

/* Only the record tier needs a dark-theme override now; the rest follow the
   --tier-* tokens, which already swap. */
[data-theme="harbor"] .record-bar-fill.tier-record,
[data-theme="harbor"] .passport-fill.tier-record {
    background: linear-gradient(90deg, #e8b830, #ffe07a 45%, #d4a017);
}

@media (max-width: 720px) {
    .record-bar { width: 100%; min-width: 90px; }
}

/* ---- Species "Worth" column ---------------------------------------------
   A number, not a shape. This column was a scaled silhouette, then two
   unlabelled bars, and both made you decode a picture to recover a quantity
   you could have just been shown. */
.worth-cell { width: 1%; }
.worth {
    display: inline-block;
    min-width: 2.6rem;
    padding: .12rem .45rem;
    border-radius: var(--r-sm, 5px);
    background: var(--h1-wash, var(--surface));
    color: var(--h1);
    font-weight: 700;
    font-size: var(--t-sm, .88rem);
    font-variant-numeric: tabular-nums;
    text-align: center;
    cursor: help;
}

/* --------------------------------------------------------------------------
   Following, made cheap

   Following someone used to be a navigation: leave the page, click, come
   back. It is now a button that appears wherever a person's name does — feed
   cards, the suggestions rail, Discover rows, the angler directory — and
   flips in place without a reload.

   That means the button has to work at two sizes and in two states without
   drawing attention away from the content it sits next to. A row of twenty
   bright primary buttons down the right edge of a table is a table you cannot
   read. So: quiet outline by default, filled only on hover, and visibly
   settled once the follow has happened.
   -------------------------------------------------------------------------- */
.btn-xs {
    padding: .3rem .68rem;
    font-size: .69rem;
    gap: .3rem;
}

.follow-form { display: inline-flex; margin: 0; }

.btn-follow {
    border-color: var(--line-strong);
    color: var(--sea);
    background: var(--card);
    font-weight: 600;
}
.btn-follow:hover {
    border-color: transparent;
    background: linear-gradient(165deg, var(--sea-bright), var(--sea-deep));
    color: #fff;
    box-shadow: var(--shadow-sm);
}

/* Already following. Deliberately flat and unassertive: the button's job here
   is to report a state, and only secondarily to offer undoing it. */
.btn-follow.is-following {
    border-color: var(--line);
    background: var(--surface-raised);
    color: var(--ink-faint);
}
.btn-follow.is-following:hover {
    border-color: rgba(176, 74, 74, .4);
    background: rgba(176, 74, 74, .07);
    color: var(--clay);
    box-shadow: none;
}

/* The glyph is drawn in CSS rather than shipped as two SVGs, because it is
   two shapes: a plus and a tick. */
.follow-glyph { position: relative; width: .62em; height: .62em; }
.follow-glyph::before,
.follow-glyph::after {
    content: '';
    position: absolute;
    background: currentColor;
    transition: transform .18s var(--ease, ease), opacity .18s;
}
.follow-glyph::before { inset: 45% 0 45% 0; }
.follow-glyph::after  { inset: 0 45% 0 45%; }
.is-following .follow-glyph::after { opacity: 0; }
.is-following .follow-glyph::before { transform: rotate(-45deg) scaleX(.8); }

/* Table cell holding one. Shrink-to-fit so it never steals width from the
   columns that carry the numbers. */
.follow-cell { width: 1%; white-space: nowrap; text-align: right; }

/* Rail suggestion: name on the left, button on the right, name truncating
   rather than pushing the button off the panel. */
.rail-angler-who { min-width: 0; flex: 1; }
.rail-angler .follow-form { flex: none; }
.rail-angler .row-title {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Feed cards from anglers you do not follow

   A minority of every feed page comes from strangers, and every one of those
   cards says so. The marker is a full-width label above the name rather than
   a chip tucked beside it, because a disclosure you only notice after you
   have read the card is not a disclosure.

   Styling stays subdued — a tinted strip, not a badge. These are ordinary
   catches by ordinary anglers, not promoted content, and dressing them up
   would make them read as ads.
   -------------------------------------------------------------------------- */
.post--new { border-color: var(--line-strong); }

/* NO FILL, and this is the corner artifact.
   .post-flag sat at the very top of .post, which is rounded and bordered. A
   square block of --surface-raised inside a rounded, bordered box gets clipped
   to the PADDING box, so its corner arc and the border's arc do not coincide —
   which leaves a faint wedge of shading at both top corners on every suggested
   post. overflow:hidden does not help; the two arcs are simply different radii.
   Removing the fill removes the wedge, and the row still reads as a flag
   because of the dot and the muted type. */
.post-flag {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin: 0;
    padding: .55rem .85rem .1rem;
    font-size: var(--t-xs, .74rem);
    color: var(--ink-faint);
    letter-spacing: .01em;
}
.post-flag time { margin-left: auto; white-space: nowrap; }
.post-flag-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--sea);
    flex: none;
}

/* The follow button sits where the timestamp normally does, so it needs the
   same "do not stretch me" treatment the timestamp had. */
.post-head .follow-form { flex: none; align-self: center; }


/* =========================================================================
   FISHING CONDITIONS
   =========================================================================

   The page has one job that the numbers alone cannot do: make it obvious at a
   glance which hours are worth getting up for, and make the reasoning visible
   rather than magical. So the rating colours are shared by the day strip, the
   window list and every hourly pill — one hue means one thing everywhere on
   the page — and the "why" tags carry their direction as a colour rather than
   as a plus sign nobody reads.

   Colours come from the existing seven-hue palette, so all six themes retint
   this section for free, exactly like the charts.
   ========================================================================= */

/* Rating scale. Used on chips, pills and the outlook strip alike. */
.rate-excellent { --rate: var(--h7); --rate-wash: var(--h7-wash, var(--surface)); }
.rate-good      { --rate: var(--h6); --rate-wash: var(--h6-wash, var(--surface)); }
.rate-fair      { --rate: var(--h4); --rate-wash: var(--h4-wash, var(--surface)); }
.rate-slow      { --rate: var(--h3); --rate-wash: var(--h3-wash, var(--surface)); }
.rate-poor      { --rate: var(--h2); --rate-wash: var(--h2-wash, var(--surface)); }

.chip.rate-excellent, .chip.rate-good, .chip.rate-fair,
.chip.rate-slow, .chip.rate-poor {
    color: var(--rate);
    border-color: var(--rate);
    background: var(--rate-wash);
}

/* ---- small utilities the forecast view needs ------------------------------
   Deliberately generic rather than forecast-prefixed: a key/value list and a
   scrollable table are not ideas peculiar to this page, and the next screen
   that wants one should not have to invent a second name for it. */
.tiny-caps {
    font-size: var(--t-xs);
    text-transform: uppercase;
    letter-spacing: .07em;
    font-weight: 700;
    color: var(--ink-mute);
    margin: 0;
}

.kv { display: grid; grid-template-columns: 1fr auto; gap: .3rem .8rem; margin: 0; }
.kv dt { color: var(--ink-mute); font-size: var(--t-sm); }
.kv dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }

/* Wide tables scroll inside their own box rather than pushing the page sideways. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-tight th, .table-tight td { padding: .38rem .55rem; }

.chip-xs { padding: .08rem .4rem; font-size: calc(var(--t-xs) * .92); }
.chip-strong { border-color: var(--h6); color: var(--h6); background: var(--h6-wash, var(--surface)); font-weight: 700; }

/* ---- the location form --------------------------------------------------- */
.forecast-where { margin-top: 1rem; }

/* ---- seven-day strip ------------------------------------------------------
   A horizontal scroller on narrow screens rather than a wrapping grid: seven
   days that reflow into 3 + 3 + 1 stop reading as a sequence, and the whole
   point of the strip is comparing along it. */
.forecast-outlook {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(6.4rem, 1fr);
    gap: .5rem;
    overflow-x: auto;
    padding-bottom: .3rem;
    scrollbar-width: thin;
}

.forecast-day {
    display: grid;
    justify-items: center;
    gap: .18rem;
    padding: .7rem .4rem;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--card);
    text-decoration: none;
    color: inherit;
    position: relative;
    transition: border-color .15s, transform .15s;
}
.forecast-day:hover { border-color: var(--line-strong); transform: translateY(-1px); }
.forecast-day.is-current { border-color: var(--rate, var(--h6)); box-shadow: 0 0 0 1px var(--rate, var(--h6)) inset; }

.fd-name  { font-weight: 700; font-size: var(--t-sm); }
.fd-date  { font-size: var(--t-xs); color: var(--ink-faint); }
.fd-score {
    font-size: 1.5rem;
    font-weight: 800;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--rate, var(--ink));
}
.fd-window { font-size: var(--t-xs); color: var(--ink-mute); font-variant-numeric: tabular-nums; }

.fd-warn {
    position: absolute;
    top: .35rem;
    right: .45rem;
    width: 1.05rem;
    height: 1.05rem;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--h2);
    color: #fff;
    font-size: .7rem;
    font-weight: 800;
}

/* ---- main / side ---------------------------------------------------------- */
.forecast-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr);
    gap: 1rem;
    align-items: start;
}
.forecast-side { display: grid; gap: 1rem; }

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

.chart-bite { width: 100%; height: auto; display: block; margin-top: .6rem; }

/* ---- best windows --------------------------------------------------------- */
.forecast-windows li {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .32rem 0;
    border-bottom: 1px solid var(--grid-1);
}
.forecast-windows li:last-child { border-bottom: 0; }
.fw-time { font-weight: 700; font-variant-numeric: tabular-nums; min-width: 7.5em; }

/* ---- hourly table --------------------------------------------------------- */
.forecast-hours { width: 100%; }
.forecast-hours tr.is-now { background: var(--h7-wash, var(--surface)); }
.forecast-hours tr.is-now th[scope="row"] { font-weight: 800; }

.score-pill {
    display: inline-flex;
    align-items: baseline;
    gap: .35rem;
    padding: .12rem .5rem;
    border-radius: 999px;
    background: var(--rate-wash, var(--surface));
    color: var(--rate, var(--ink));
    border: 1px solid var(--rate, var(--line));
    font-size: var(--t-xs);
    font-weight: 600;
    white-space: nowrap;
}
.score-pill b { font-size: var(--t-sm); font-weight: 800; font-variant-numeric: tabular-nums; }

/* The "why" tags. Direction is carried by colour and by the sign, not by
   colour alone — a red/green-only cue is invisible to a good number of people. */
.factor-tag {
    display: inline-flex;
    align-items: baseline;
    gap: .22rem;
    margin-right: .3rem;
    white-space: nowrap;
    font-size: var(--t-xs);
    color: var(--ink-mute);
}
.factor-tag b { font-variant-numeric: tabular-nums; }
.factor-tag[data-sign="up"]   b { color: var(--h6); }
.factor-tag[data-sign="down"] b { color: var(--h2); }

/* ---- safety --------------------------------------------------------------
   Loud on purpose. This is the one block on the page allowed to shout. */
.forecast-safety {
    margin-top: 1rem;
    border-left: 4px solid var(--h4);
    background: var(--h4-wash, var(--surface));
}
.forecast-safety[data-level="danger"] {
    border-left-color: var(--h2);
    background: var(--h2-wash, var(--surface));
}
.forecast-safety strong { color: var(--ink); }

/* Evidence marks in the "how" table, so the reader can see at a glance which
   factors are research-backed and which are folklore. */
[data-evidence="observed"]    { color: var(--h6); }
[data-evidence="plausible"]   { color: var(--h4); }
[data-evidence="traditional"] { color: var(--ink-faint); font-style: italic; }


/* ── Rank emblems ─────────────────────────────────────────────────────────
   The badge is a fixed-aspect SVG generated by RankEmblem. These rules only
   size and seat it — everything visual is in the drawing, so the two products
   cannot drift apart by one of them restyling its copy.

   IT IS PORTRAIT, 128x168. A decoration hangs from a ribbon, so the SVG carries
   its own width AND height attributes and callers pass a WIDTH. Anything here
   that reserves space has to reserve 1.31x that in height — which is why the
   row variants align on the medal rather than on the box.

   NOTE ON SIZE: below about 30px the ribbon stripes and the grade ring stop
   resolving and the badge reads as a coloured seal. The inline variant is
   deliberately that — a silhouette-scale mark next to a name, not a legible
   medal. For the real thing the profile uses 84px and the ranks table 46px. */
.rank-emblem { display: inline-flex; line-height: 0; flex: none; }

/* The medal column on a leaderboard. Narrow, and it collapses on a phone —
   a board on a 375px screen has room for a face, a name and a number, and the
   medal is the one of the four that repeats information already in the row. */
.medal-cell { width: 1%; padding-left: .25rem; padding-right: .25rem; }
.rank-medal { display: inline-flex; line-height: 0; }

/* The rail card's medal sits beside the rank name, not above it — the card is
   narrow and stacking them would push the points figure below the fold of a
   panel that exists to be read at a glance. */
.rail-rank { align-items: center; gap: .7rem; margin-bottom: .1rem; }
.rail-rank .rail-points { margin: .1rem 0 0; }
/* max-width: none, and it is the whole reason this renders at all.
   
   The global reset is `img, svg, video { height: auto; max-width: 100% }`,
   which is right for a photograph and wrong for an icon carrying its own
   intrinsic size. In a `width: 1%` table cell the containing block resolves to
   nothing, 100% of nothing is nothing, and the medal drew at 0x0 — present in
   the DOM, correct in every other respect, and invisible. Letting the width
   and height ATTRIBUTES do their job is the fix.
   
   height: auto stays — the SVG's own height attribute already carries the
   portrait ratio, and forcing a square here would squash the ribbon. */
.rank-medal svg {
    display: block;
    max-width: none;
    width: auto;
    height: auto;
}

/* A leaderboard row is denser than the ranks table, so its medal is small
   enough (34px wide, 45 tall) to sit inside the row's own height without
   pushing it. No negative margin — see .rank-emblem-row for why that clips. */

@media (max-width: 720px) {
    .medal-cell { display: none; }
}
.rank-emblem svg { display: block; filter: drop-shadow(0 2px 5px rgba(11,17,24,.28)); }

/* A TALL BADGE SETS ITS ROW'S HEIGHT, and on the ranks table that is correct —
   the medal is what the page is about. It is NOT pulled up with a negative
   margin: that was tried, and the row clipped the ribbon off the top and the
   medal off the bottom, because a table cell will not let its content spill. */
.rank-emblem-row {
    vertical-align: middle;
    margin-right: .5rem;
}
.rank-emblem-inline { vertical-align: -.35em; margin: 0 .12rem; }
.rank-emblem-inline svg { filter: none; }




/* The emblem standing in for an avatar — on boards and on posts. */
.rank-emblem-avatar { flex: none; display: inline-flex; line-height: 0; }
.rank-emblem-avatar svg { display: block; }
.post-points { font-size: var(--t-xs); font-weight: 700; margin-left: .35rem;
               font-variant-numeric: tabular-nums; }
.post-points-unit { font-weight: 500; opacity: .72; margin-left: .18rem; }




/* ── Points are green ─────────────────────────────────────────────────────
   Every points figure on the site, one colour. Rank-coloured figures made the
   number mean two things at once — how many points, and which material — and
   the pale metals (diamond, platinum, silver) could not carry small type at
   all. Green is the one hue not already spoken for: sea is links and primary
   actions, gold is the brand, clay is destructive, mark is trophy scores. */
:root, [data-theme] { --points-green: #147A55; }
[data-theme="harbor"] { --points-green: #4FD3A0; }

.points-ink, .post-points { color: var(--points-green); }
.post-points-unit { color: inherit; }

/* .points-ink is COLOUR ONLY, on purpose. .points-value is the big display
   figure — clamp(1.5rem, 4vw, 2rem) — and putting that on a leaderboard cell
   would triple the row height. A board wants the colour and its own size. */
.points-ink { font-variant-numeric: tabular-nums; }


/* ==========================================================================
   THE WAITING SCREEN
   --------------------------------------------------------------------------
   Covers two gaps where the site otherwise looks broken rather than busy:
   an upload in flight, and a navigation that is taking longer than a tap
   should. Markup in app/Views/partials/loader.php, behaviour in app.js.

   ABOVE EVERYTHING, INCLUDING THE LIGHTBOX (2000). It is the only element on
   the site that means "the browser is doing something and nothing you press
   now will help" — a panel that something else can cover is not saying that.

   NOT AN OPAQUE PANEL. The page behind stays visible through the blur, which
   is what makes it read as a pause rather than as a new screen: whatever was
   being uploaded or navigated away from is still faintly there.
   ========================================================================== */
/* THIS RULE IS NOT OPTIONAL AND IT MUST COME FIRST.
   `hidden` is an attribute, and the only thing implementing it is the user
   agent stylesheet's `[hidden] { display: none }` — which is the WEAKEST
   declaration in the cascade. The `display: grid` below is an author rule on
   a class, so it wins, and without this line the panel is on screen on every
   page of the site the moment it is added to the layout: an opaque overlay
   nobody can click through, covering a page that is working perfectly. */
.loader[hidden] { display: none; }

.loader {
    position: fixed;
    inset: 0;
    z-index: 3000;
    display: grid;
    place-items: center;
    padding: 1.5rem;

    /* A SCRIM, NOT A TINTED SHEET OF --paper. The first version veiled the
       page with the page's own background colour, which is invisible by
       definition: on the dark theme --paper IS what is behind it, so the
       overlay and the card standing on it both disappeared and the wording
       was left floating in mid-air. This is the same shadow-based scrim the
       welcome dialog uses — darkness rather than a colour, so it reads on
       both themes. */
    background: rgba(var(--shadow-rgb), .5);
    -webkit-backdrop-filter: blur(6px) saturate(1.05);
    backdrop-filter: blur(6px) saturate(1.05);

    /* The panel fades IN over a beat, so a page that arrives in 200 ms after
       the timer fired never resolves into a hard flash of interface. It is
       the difference between "the site paused" and "the site flickered". */
    animation: loader-in .22s ease-out both;
}

/* The porthole's own palette, so light and dark both get real water rather
   than one theme's colours pushed through an opacity. */
.loader {
    --loader-sky:  #2f8fbe;
    --loader-deep: #06496d;
    --loader-rim:  rgba(255, 255, 255, .55);
}
[data-theme="harbor"] .loader {
    --loader-sky:  #17557d;
    --loader-deep: #041f31;
    --loader-rim:  rgba(173, 218, 245, .38);
}

@keyframes loader-in { from { opacity: 0; } to { opacity: 1; } }

/* Matched to .welcome-card, because they are the same kind of object: a
   small panel over a dimmed page. --card rather than --paper for the fill —
   --paper is the page itself, and a panel the same colour as its background
   has no edges on the dark theme. */
.loader-card {
    width: min(340px, 100%);
    text-align: center;
    padding: 1.9rem 1.6rem 1.7rem;
    border-radius: var(--r-lg, 18px);
    background: var(--card);
    border: 1px solid var(--line);
    box-shadow: 0 24px 60px rgba(var(--shadow-rgb), .3);
}

.loader-scene { width: 92px; height: 92px; margin: 0 auto .95rem; }
.loader-svg   { width: 100%; height: 100%; display: block; }

/* ---- The tide ----------------------------------------------------------
   Two sines, one 128 units wide inside a 64-wide viewBox, translating by
   exactly half their width. Half a width is one full period of this path, so
   the loop is seamless — a translate of anything else pops on repeat.

   They run at different speeds and in opposite directions. That is the whole
   trick: two waves that never line up twice read as water, and two waves in
   step read as a sliding stripe. */
.loader-wave { fill: var(--loader-sky); }
.loader-wave-back  { opacity: .45; animation: loader-tide-back 4.6s linear infinite; }
.loader-wave-front { opacity: .8;  animation: loader-tide-front 3.1s linear infinite; }

@keyframes loader-tide-back  { from { transform: translateX(-64px); } to { transform: none; } }
@keyframes loader-tide-front { from { transform: none; } to { transform: translateX(-64px); } }

/* The whole water group rises and falls a little, so the level is a tide
   coming in over the fins rather than a fixed line with texture on it. */
.loader-water { animation: loader-swell 3.8s ease-in-out infinite; }
@keyframes loader-swell {
    0%, 100% { transform: translateY(3px); }
    50%      { transform: translateY(-6px); }
}

/* ---- Bubbles -----------------------------------------------------------
   Three, not thirty. This is 92 px across; any more and it is texture. */
.loader-bubbles circle { animation: loader-bubble 3.4s ease-in infinite; }
.loader-bubbles .b2 { animation-duration: 4.4s; animation-delay: -1.6s; }
.loader-bubbles .b3 { animation-duration: 2.9s; animation-delay: -2.4s; }

/* Sized in USER UNITS, not pixels: these are SVG children in a 64-unit
   viewBox, so a translate of 46 would carry a bubble two thirds of the way up
   a disc only 64 tall in its own coordinates.

   AND IT HAS TO DIE AT THE SURFACE. The bubbles are drawn before the waves so
   the crest passes over them, but above the waterline there is nothing to
   hide behind and a bubble that keeps climbing is a bubble in mid-air. 15
   units carries one from the floor to roughly where the tide sits, and the
   fade is finished before it gets there. */
@keyframes loader-bubble {
    0%   { transform: translateY(0)     translateX(0); opacity: 0; }
    18%  { opacity: .7; }
    72%  { opacity: .35; }
    100% { transform: translateY(-15px) translateX(2px); opacity: 0; }
}

/* A slow breath on the mark, so the gold catches differently as the water
   moves over it. */
.loader-fins {
    /* fill-box so "bottom centre" means the bottom of the fins rather than
       the corner of the whole 64-unit canvas — the mark should breathe from
       the waterline it stands on. */
    transform-box: fill-box;
    transform-origin: bottom center;
    animation: loader-glint 3.8s ease-in-out infinite;
}
@keyframes loader-glint {
    0%, 100% { opacity: .92; transform: scale(1); }
    50%      { opacity: 1;   transform: scale(1.035); }
}

.loader-title {
    margin: 0;
    font-weight: 600;
    font-size: var(--t-md, 1.0625rem);
    color: var(--ink);
}

.loader-note {
    margin: .3rem 0 0;
    font-size: var(--t-sm);
    color: var(--ink-faint, var(--ink-soft));
    font-variant-numeric: tabular-nums;
}

/* ---- The bar -----------------------------------------------------------
   Indeterminate on purpose. A native form post reports no upload progress to
   the page, and inventing a percentage that sticks at 90% is a worse lie
   than a bar that simply keeps moving. */
.loader-bar {
    position: relative;
    height: 4px;
    margin-top: 1.15rem;
    border-radius: 999px;
    overflow: hidden;
    background: var(--surface-raised, rgba(0, 0, 0, .07));
}
.loader-bar i {
    position: absolute;
    inset-block: 0;
    width: 40%;
    border-radius: inherit;
    background: linear-gradient(90deg,
                transparent, var(--sea, #0a6e9e), var(--sea-bright, #1a8cc0), transparent);
    animation: loader-sweep 1.35s ease-in-out infinite;
}
@keyframes loader-sweep {
    from { transform: translateX(-110%); }
    to   { transform: translateX(360%); }
}

/* ---- Reduced motion ----------------------------------------------------
   The panel STAYS. Somebody who asked for less movement still needs to be
   told the site is busy — the request is about motion sickness, not about
   feedback. So the tide, the bubbles and the sweep stop, and the bar breathes
   in place instead. */
@media (prefers-reduced-motion: reduce) {
    .loader { animation: none; }
    .loader-wave,
    .loader-water,
    .loader-bubbles circle,
    .loader-fins { animation: none; }
    .loader-bar i {
        width: 100%;
        animation: loader-pulse 1.6s ease-in-out infinite;
    }
    @keyframes loader-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
}

@media print { .loader { display: none !important; } }

/* THE ANDROID "GET THE APP" BAR — partials/app-banner.php.
   In the flow above the masthead, so it is the first thing on the page and
   covers nothing: it scrolls away and the sticky masthead takes the top.
   Tokens throughout, so it follows the reader's theme. */
.app-banner {
    position: relative;
    z-index: 101;
    background: var(--card);
    border-bottom: 1px solid var(--line);
}
[data-hide-app-banner] .app-banner { display: none; }
.app-banner-inner {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding-block: .55rem;
}
.app-banner-icon { width: 40px; height: 40px; border-radius: 10px; flex: none; }
.app-banner-text { flex: 1; min-width: 0; margin: 0; line-height: 1.25; }
.app-banner-text strong,
.app-banner-text span {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
.app-banner-text strong { font-size: var(--t-sm); color: var(--ink); }
.app-banner-text span   { font-size: var(--t-xs); color: var(--ink-mute); }
/* 40px, not the 24px of the glyph: the whole square is the tap target. */
.app-banner-close {
    flex: none;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    margin-right: -.5rem;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--ink-mute);
    cursor: pointer;
}
.app-banner-close:hover { color: var(--ink); background: var(--paper-sunk); }
.app-banner-close svg { width: 20px; height: 20px; }

/* ==========================================================================
   Trophy wall — the trophy room, and the true-size comparison
   ========================================================================== */
.trophy-room { padding-bottom: 3rem; }
.trophy-filters { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin: 0 0 1.2rem; }
.trophy-filters .select { width: auto; min-width: 11rem; }
.trophy-filters .seg-btn { text-decoration: none; }

/* The room: dark panelling, warm light from above, the same in both themes —
   a trophy room is a dark room. */
.trophy-wall {
    position: relative; border-radius: var(--r-lg); overflow: hidden;
    padding: clamp(1.2rem, 3vw, 2.4rem);
    color: #f4ead7;
    background:
        radial-gradient(60% 40% at 22% 0%, rgba(255, 214, 150, .22), transparent 70%),
        radial-gradient(60% 40% at 78% 0%, rgba(255, 214, 150, .18), transparent 70%),
        repeating-linear-gradient(90deg, rgba(0, 0, 0, .18) 0 2px, transparent 2px 118px),
        repeating-linear-gradient(90deg, rgba(255, 255, 255, .025) 0 1px, transparent 1px 7px),
        linear-gradient(180deg, #3b2718 0%, #2c1d12 55%, #22160d 100%);
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .35), inset 0 -40px 60px rgba(0, 0, 0, .25), var(--shadow-sm);
}
.trophy-empty { text-align: center; padding: 3rem 1rem; display: grid; gap: 1rem; justify-items: center; color: #e9dcc4; }

/* A plaque: an oval of polished wood, the fish mounted on it, a brass plate. */
.plaque {
    position: relative; display: flex; flex-direction: column; align-items: center; gap: .7rem;
    color: inherit; text-decoration: none;
    transform: rotate(var(--tilt, 0deg)); transition: transform .2s ease;
}
.plaque:hover { transform: rotate(0deg) translateY(-3px); }
.plaque-nail {
    width: 10px; height: 10px; border-radius: 50%; margin-bottom: -4px; z-index: 2;
    background: radial-gradient(circle at 35% 30%, #fff3cf, #b98a3a 55%, #5e4219);
    box-shadow: 0 2px 3px rgba(0, 0, 0, .5);
}
.plaque-board {
    position: relative; width: 100%; aspect-ratio: 16 / 9; border-radius: 50%;
    display: grid; place-items: center;
    background:
        radial-gradient(70% 60% at 40% 30%, rgba(255, 230, 190, .28), transparent 70%),
        repeating-linear-gradient(100deg, rgba(0, 0, 0, .10) 0 3px, transparent 3px 9px),
        linear-gradient(160deg, #8a5a32, #6b4224 45%, #4c2e17);
    box-shadow:
        inset 0 0 0 6px rgba(0, 0, 0, .18), inset 0 0 0 8px rgba(255, 220, 170, .18),
        0 14px 24px rgba(0, 0, 0, .5), 0 3px 6px rgba(0, 0, 0, .35);
}
.plaque-fish {
    width: 80%; max-height: 70%; object-fit: contain;
    filter: drop-shadow(0 8px 6px rgba(0, 0, 0, .45)) drop-shadow(0 2px 2px rgba(0, 0, 0, .35));
}
.plaque-photo {
    position: absolute; right: 2%; bottom: -6%; width: 26%; aspect-ratio: 1; object-fit: cover;
    border: 4px solid #f7f1e3; border-bottom-width: 12px; border-radius: 2px;
    transform: rotate(6deg); box-shadow: 0 6px 12px rgba(0, 0, 0, .45);
}
.plaque-score {
    position: absolute; top: 10%; right: 12%;
    font-size: .74rem; font-weight: 800; padding: .15rem .5rem; border-radius: 999px; color: #fff;
    background: var(--tier-logged); box-shadow: 0 2px 6px rgba(0, 0, 0, .4);
}
.plaque-score[data-tier="record"]    { background: var(--tier-record); }
.plaque-score[data-tier="legendary"] { background: var(--tier-legendary); }
.plaque-score[data-tier="trophy"]    { background: var(--tier-trophy); }
.plaque-score[data-tier="notable"]   { background: var(--tier-notable); }
.plaque-brass {
    display: grid; gap: .1rem; text-align: center; min-width: 70%; padding: .5rem .9rem .55rem;
    border-radius: 6px; color: #2b1d08;
    background: linear-gradient(180deg, #f6dd9a, #d8ad4e 48%, #b88a2c 52%, #e2bf6a);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6), inset 0 -1px 0 rgba(0, 0, 0, .25), 0 4px 8px rgba(0, 0, 0, .45);
    text-shadow: 0 1px 0 rgba(255, 255, 255, .35);
}
.plaque-brass strong { font-family: var(--font-display); font-size: 1.05rem; line-height: 1.15; }
.plaque-measure { font-weight: 700; font-size: .85rem; font-variant-numeric: tabular-nums; }
.plaque-age { font-size: .78rem; font-weight: 600; }
.plaque-who { font-size: .72rem; opacity: .8; }
.plaque-kicker { font-size: .66rem; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; opacity: .75; }

.plaque-grid {
    display: grid; gap: clamp(1.4rem, 3vw, 2.4rem) clamp(1rem, 2.5vw, 2rem); margin-top: clamp(1.6rem, 4vw, 3rem);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr));
}

.trophy-featured { display: grid; gap: clamp(1.2rem, 3vw, 2.4rem); align-items: center; grid-template-columns: 1fr; }
@media (min-width: 900px) { .trophy-featured { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); } }
.plaque-lg .plaque-brass strong { font-size: 1.35rem; }
.trophy-facts {
    display: grid; gap: 1rem; padding: 1.1rem 1.2rem; border-radius: var(--r);
    background: rgba(255, 246, 228, .96); color: var(--ink, #0a1922); box-shadow: 0 16px 36px rgba(0, 0, 0, .45);
}
.trophy-facts .k { display: block; font-size: .7rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: #8a6a3a; }
.fact-big b { display: block; font-family: var(--font-display); font-size: 1.6rem; line-height: 1.15; }
.trophy-facts .btn { justify-self: start; }
.trophy-how { margin-top: 1.4rem; }
.trophy-how summary { cursor: pointer; font-weight: 600; }
.trophy-how p { margin-top: .6rem; color: var(--ink-soft); line-height: 1.6; }

/* ---- The true-size comparison (partials/size-compare.php) ---------------- */
.size-compare {
    position: relative; width: 100%; margin: 0;
    background:
        linear-gradient(180deg, transparent calc(100% - 1px), rgba(10, 60, 90, .18) 0) 0 0 / 100% 100%,
        linear-gradient(180deg, #eef5f9, #dbe9f1);
    border-radius: 10px; overflow: visible;
}
.size-compare > * { position: absolute; }
.sc-ref { fill: #6f8796; }
.sc-ref .cap { fill: #54707f; }
.sc-phone rect { fill: #33424c; }
.sc-phone .screen { fill: #8fb4c9; }
.sc-fish { height: auto; filter: drop-shadow(0 4px 3px rgba(0, 0, 0, .22)); }
.sc-record { opacity: .34; filter: grayscale(.6) drop-shadow(0 2px 2px rgba(0, 0, 0, .15)); }
.size-compare-wrap { margin: .4rem 0 0; }
.sc-tag { font-size: .7rem; line-height: 1.2; color: #5b7380; white-space: nowrap; }
.sc-tag b { color: #2e4653; }
.sc-tape { height: 7px; }
.sc-tape rect { fill: #f2c34a; }
.sc-tape line { stroke: #6b4a10; stroke-width: .35; vector-effect: non-scaling-stroke; }
.sc-len { transform: translate(-50%, 115%); font-size: .8rem; font-weight: 800; color: #2e4653; white-space: nowrap; }
.size-compare-wrap figcaption {
    display: flex; flex-wrap: wrap; gap: .3rem .9rem; margin-top: 1.9rem;
    font-size: .74rem; color: var(--ink-mute, #5b7380);
}
.sc-key { display: inline-flex; align-items: center; gap: .35rem; }
.sc-key i { width: 12px; height: 8px; border-radius: 4px; display: inline-block; }
.sc-key .k-fish { background: #0a6e9e; }
.sc-key .k-record { background: rgba(10, 110, 158, .3); }
.sc-key .k-ref { background: #6f8796; }
.sc-note { width: 100%; }

/* The catch page's own block. */
.catch-size { display: grid; gap: 1rem; }
.catch-size .age-line { font-family: var(--font-display); font-size: 1.35rem; }
