/* Panel gradients, site-wide.
 *
 * A page that stacks full-height sections wants two things that pull against
 * each other: each section should feel like its own room, and the colour should
 * not step where two of them meet. Doing it per-section gives you the rooms and
 * a visible seam at every join, because the same boundary colour is written
 * twice and the two copies drift. Doing it as one background across everything
 * gives you the smooth colour and no rooms at all.
 *
 * So the base ramp belongs to the container and the character belongs to the
 * panels. The ramp is continuous, which makes every seam exact by construction
 * rather than by matching hex values. Each panel adds blooms on top, and those
 * are masked to fade out before its own edges -- so at the boundary itself both
 * sides contribute nothing and only the ramp is showing. The step is zero
 * because nothing but the ramp is ever painted there.
 *
 *   <div class="gdPanels">
 *     <section class="gdPanel">...</section>
 *     <section class="gdPanel">...</section>
 *   </div>
 *
 * Nothing draws a horizontal anything at a boundary -- no rule, no glow, no
 * band. Those were all tried and all read as a line: a hairline measured 180 on
 * a channel-sum and even a glow that faded in from transparent left something
 * the eye picks up, because a thin feature spanning the full width is visible
 * long before its amplitude sounds like much. Colour is the whole transition.
 *
 * Everything below is a custom property with a default, so a panel with no
 * configuration is a plain slice of the ramp rather than something broken.
 */

/* The scheme for this page load. shared/php/palette.php emits --gp-h; without
   it this default applies, which is the blue the palette was originally built
   on, so a page that does not opt in looks exactly as it always did.

   Everything is derived at fixed OKLCH lightness and chroma, so only the hue
   moves. Perceptual lightness held still means contrast held still: a panel is
   the same distance from white text at any hue. The offsets are the ones the
   hand-picked palette measured at -- the warm accent is the base's exact
   complement, which is what ember was to slate. */
:root {
    --gp-h: 242;
    --gp-spread: 1;
    --gp-sat: 1;

    /* The ramp's own stops, exposed so whatever sits above and below a run of
       panels can hand off to exactly what it ends on rather than to a hex that
       only matches at one hue. */
    --gp-ramp-from: oklch(0.320 calc(0.032 * var(--gp-sat)) calc((var(--gp-h)) * 1deg));
    --gp-ramp-via:  oklch(0.297 calc(0.074 * var(--gp-sat)) calc((calc(var(--gp-h) + 58 * var(--gp-spread))) * 1deg));
    --gp-ramp-to:   oklch(0.184 calc(0.015 * var(--gp-sat)) calc((calc(var(--gp-h) + 11 * var(--gp-spread))) * 1deg));

    /* Accents, for anything drawn ON a panel -- rules, rails, links, the run.
       Same lightness and hue offsets the brand three measured at: ember is the
       base's complement, rose and magenta step back from it. Because their
       lightness is fixed near 0.6-0.73 and a panel's is 0.18-0.32, an accent
       cannot land too close to its background at any hue. --gd-* stays the
       fixed brand palette, for the logo and anything that must not rotate. */
    --gp-ember:   oklch(0.732 calc(0.139 * var(--gp-sat)) calc((calc(var(--gp-h) + 181 * var(--gp-spread))) * 1deg));
    --gp-rose:    oklch(0.631 calc(0.173 * var(--gp-sat)) calc((calc(var(--gp-h) + 134 * var(--gp-spread))) * 1deg));
    --gp-magenta: oklch(0.598 calc(0.188 * var(--gp-sat)) calc((calc(var(--gp-h) + 120 * var(--gp-spread))) * 1deg));
    --gp-run: linear-gradient(100deg,
        var(--gp-ember), var(--gp-rose) 55%, var(--gp-magenta));

    /* Paper, for cards sitting on a panel. Tinted by the hue rather than pure
       white, and a little off it, so a card reads as a surface in the same
       light rather than a hole cut in the page. */
    --gp-card:     oklch(0.962 calc(0.009 * var(--gp-sat)) calc((calc(var(--gp-h) + 181 * var(--gp-spread))) * 1deg));
    --gp-card-mid: oklch(0.941 calc(0.011 * var(--gp-sat)) calc((calc(var(--gp-h) + 181 * var(--gp-spread))) * 1deg));
    --gp-card-low: oklch(0.918 calc(0.014 * var(--gp-sat)) calc((calc(var(--gp-h) + 181 * var(--gp-spread))) * 1deg));

    /* Thin marks: a rail, an underline, a hairline between two words. These
       need more separation than the accents above give them, and for a reason
       the accents cannot fix. A panel's lightness is not one number -- the
       blooms lift patches of it, and #overview's cool bloom lifts exactly the
       strip jens's timeline runs down. Measured against that lifted ground
       round the whole hue circle, the accent run reads 2.8:1 at ember and
       1.6:1 at magenta, so the bottom of a rail and the right end of an
       underline are the parts that disappear.

       These are the same three hues held high enough to clear it: 4.5:1 or
       better at every hue, which is the text threshold -- jens's break label
       is set in one of them. Lightness is what holds that, which is why
       --gp-sat moving the chroma cannot take it away. */
    --gp-mark-a: oklch(0.880 calc(0.095 * var(--gp-sat)) calc((calc(var(--gp-h) + 181 * var(--gp-spread))) * 1deg));
    --gp-mark-b: oklch(0.855 calc(0.105 * var(--gp-sat)) calc((calc(var(--gp-h) + 134 * var(--gp-spread))) * 1deg));
    --gp-mark-c: oklch(0.850 calc(0.110 * var(--gp-sat)) calc((calc(var(--gp-h) + 120 * var(--gp-spread))) * 1deg));
    --gp-mark-run: linear-gradient(100deg,
        var(--gp-mark-a), var(--gp-mark-b) 55%, var(--gp-mark-c));

    /* A dark strip to draw a mark on where lightness alone is not enough --
       the same idea as the card, at the width of a hairline: a surface of its
       own rather than a colour hoping to win against whatever is behind it. */
    --gp-plate: oklch(0.170 calc(0.020 * var(--gp-sat)) calc((var(--gp-h)) * 1deg));

    /* A track is the unlit half of a rail: dimmer than a mark on purpose, but
       still visible. It has to be lighter than the panel ever gets, not
       darker than the mark -- a mid-lightness colour is worse than the white
       at 16% it replaces, because at 0.56 it lands next to the ground it is
       meant to sit on and measures 1.6:1. This one holds 3.4:1. */
    --gp-track: oklch(0.760 calc(0.030 * var(--gp-sat)) calc((var(--gp-h)) * 1deg));

    --gp-cool: oklch(0.57 calc(0.061 * var(--gp-sat)) calc((var(--gp-h)) * 1deg));
    --gp-deep: oklch(0.38 calc(0.096 * var(--gp-sat)) calc((calc(var(--gp-h) + 95 * var(--gp-spread))) * 1deg));
    --gp-rich: oklch(0.60 calc(0.188 * var(--gp-sat)) calc((calc(var(--gp-h) + 120 * var(--gp-spread))) * 1deg));
    --gp-warm: oklch(0.73 calc(0.139 * var(--gp-sat)) calc((calc(var(--gp-h) + 180 * var(--gp-spread))) * 1deg));

    /* Where a dawn out of the panels lands, and the paper past it. Both sit on
       the warm complement, which is where the hand-picked peach was. */
    /* 0.058, not the 0.072 the hand-picked peach measured at. The same chroma
       reads far more vividly in mint or pink at this lightness than it does in
       orange, so the measured value made some hues candy-bright while the
       original looked soft. This is the one number that controls how bold a
       dawn gets; 0.072 restores the original peach exactly. */
    --gp-dawn:  oklch(0.871 calc(0.058 * var(--gp-sat)) calc((calc(var(--gp-h) + 181 * var(--gp-spread))) * 1deg));
    --gp-paper: oklch(0.950 calc(0.004 * var(--gp-sat)) calc((var(--gp-h)) * 1deg));

    --gd-ink:     #101114;
    --gd-slate:   #3a4d58;
    --gd-ember:   #e5933e;
    --gd-coral:   #e36e58;
    --gd-rose:    #de5366;
    --gd-magenta: #d34077;
    --gd-plum:    #622c55;
    --gd-paper:   #e8e6e9;
    --gd-run:     linear-gradient(100deg, var(--gd-ember), var(--gd-rose) 55%, var(--gd-magenta));
}

/* ---- the container: one ramp for however many panels sit inside it ---- */
.gdPanels {
    --gdp-from:  var(--gp-ramp-from);
    --gdp-via:   var(--gp-ramp-via);
    --gdp-to:    var(--gp-ramp-to);
    --gdp-via-at: 52%;
    --gdp-angle: 178deg;
    --gdp-grain: 0.04;

    position: relative;
    isolation: isolate;

    /* oklab, not sRGB. Slate to plum through sRGB dips through a muddy grey at
       the midpoint because it interpolates channels rather than colour; over
       several screens that midpoint is a whole viewport. */
    background: linear-gradient(var(--gdp-angle) in oklab,
        var(--gdp-from) 0%,
        var(--gdp-via) var(--gdp-via-at),
        var(--gdp-to) 100%);
}

/* Grain, once, across the whole run. A ramp this long bands on an 8-bit
   display; a few percent of noise breaks the stair-stepping up. Not blended --
   a blend layer at this size costs a backdrop read every frame. */
.gdPanels::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    opacity: var(--gdp-grain);
    /* Faded at both ends. The noise field stopping dead at the wrapper's edge
       is itself an edge -- it put 6 of the 10 measured at the last boundary,
       because the eye reads "noise here, none there" as a line even though no
       colour changed. */
    -webkit-mask-image: linear-gradient(180deg,
        transparent 0, #000 12vh, #000 calc(100% - 12vh), transparent 100%);
    mask-image: linear-gradient(180deg,
        transparent 0, #000 12vh, #000 calc(100% - 12vh), transparent 100%);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
}

/* ---- a panel: what makes this section this section ---- */
.gdPanel {
    --gdp-bloom-a:      transparent;
    --gdp-bloom-a-at:   14% 8%;
    --gdp-bloom-a-size: 76% 60%;
    --gdp-bloom-b:      transparent;
    --gdp-bloom-b-at:   86% 90%;
    --gdp-bloom-b-size: 72% 56%;

    /* Optional third and fourth layers, for a panel that should carry more than
       two blooms. A conic sweep reads differently from a radial -- it turns
       rather than spreads -- and the sheen takes the long way round the hue
       circle, so it passes through the colours between its two stops instead of
       fading between them. Both off unless a page asks. */
    --gdp-sweep:        transparent;
    --gdp-sweep-at:     50% 50%;
    --gdp-sheen-from:   transparent;
    --gdp-sheen-to:     transparent;
    --gdp-sheen-angle:  163deg;

    /* How far the blooms fade before the panel's own edges. This is the whole
       reason the seams are clean, so it is not decorative: at 0 the blooms
       reach the boundary and the step comes back. 30% is where it measured
       flat -- 15% and 20% still left a couple of points, and 35% changed
       nothing further. Place blooms inside the middle 40% rather than out at
       the edges, or this fade is what you will be looking at. */
    --gdp-fade: 30%;


    position: relative;
    z-index: 2;

    /* A panel never paints its own background -- the ramp underneath is the
       background, and anything opaque here puts the seam straight back.
       !important because this is the mechanism, not a preference, and a
       product's own section classes will outrank a single class by
       specificity: main's .sect.two sets a flat #2B5C6E and won, which painted
       over the ramp entirely. A page that wants a solid section should leave
       .gdPanel off it rather than fight this. */
    background: transparent !important;
}

/* Blooms. Negative z-index puts them behind the panel's content and in front of
   the container's ramp, which is the only place they belong. */
.gdPanel::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(var(--gdp-bloom-a-size) at var(--gdp-bloom-a-at),
            var(--gdp-bloom-a) 0%, transparent 72%),
        radial-gradient(var(--gdp-bloom-b-size) at var(--gdp-bloom-b-at),
            var(--gdp-bloom-b) 0%, transparent 72%),
        conic-gradient(from 210deg at var(--gdp-sweep-at),
            transparent 0turn, var(--gdp-sweep) 0.28turn, transparent 0.62turn),
        linear-gradient(var(--gdp-sheen-angle) in oklch longer hue,
            var(--gdp-sheen-from) 12%, var(--gdp-sheen-to) 88%);
    -webkit-mask-image: linear-gradient(180deg,
        transparent 0%, #000 var(--gdp-fade),
        #000 calc(100% - var(--gdp-fade)), transparent 100%);
    mask-image: linear-gradient(180deg,
        transparent 0%, #000 var(--gdp-fade),
        #000 calc(100% - var(--gdp-fade)), transparent 100%);
}

/* Links in a panel's own copy. A panel is darker than the flat colours these
   sections used to carry, so a link colour that was legible on mid-teal can
   fail on the ramp -- on the gaming page an unstyled anchor went from 7.18 to
   1.01. Scoped to headings, paragraphs and list items on purpose: a card that
   is itself an anchor, like a lab tile or a product block, brings its own
   colours and must not be repainted. */
.gdPanel h1 > a,
.gdPanel h2 > a,
.gdPanel h3 > a,
.gdPanel h4 > a,
.gdPanel p > a,
.gdPanel li > a {
    color: var(--gp-ember);
    text-decoration-color: color-mix(in oklab, var(--gp-ember) 45%, transparent);
}
.gdPanel h1 > a:hover,
.gdPanel h2 > a:hover,
.gdPanel h3 > a:hover,
.gdPanel h4 > a:hover,
.gdPanel p > a:hover,
.gdPanel li > a:hover {
    color: var(--gp-rose);
}

/* ---- a dawn: a run of panels resolving up into a light section ----
 *
 * The straight version of this was a horizon ruled with a set square. A
 * gradient boundary is exactly level and exactly the same all the way across,
 * which is the one thing no real horizon is, and the eye reads it as a band
 * rather than as light.
 *
 * So the transition is displaced by turbulence. The mask draws the vertical
 * fade and runs it through feDisplacementMap, which shifts each pixel by an
 * amount read out of a noise field, so the straight line comes out soft and
 * irregular and different along its whole length -- without a single blob
 * placed by hand.
 *
 * Two details that are not optional. The source rect is drawn far outside the
 * viewBox, because displacement samples beyond its own edges and the first
 * version pulled transparency in from outside as black wedges, which punched
 * holes in the settled colour at the bottom corners. And baseFrequency is
 * lopsided -- low across, higher down -- so the wander is wide and lazy rather
 * than a fringe of small teeth.
 *
 * It cannot introduce a seam: the mask only decides how much of the settled
 * colour shows, and that colour is the same one the ramp beneath resolves to,
 * so wherever the mask is fully on or fully off there is nothing to step.
 *
 * Where the horizon sits is a contrast decision, not a taste one. Dropped to
 * 40-60% it looked its best and put the heading below it on the coral at 4.23
 * against #333, under AA. At 24-44% the settled colour reaches the heading and
 * it measures 7.70.
 */
.gdDawn {
    position: relative;
    isolation: isolate;
}

.gdDawn::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background: var(--gp-dawn);
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cfilter id='d' x='-50%25' y='-50%25' width='200%25' height='200%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.007 0.026' numOctaves='4' seed='11' result='n'/%3E%3CfeDisplacementMap in='SourceGraphic' in2='n' scale='24' xChannelSelector='R' yChannelSelector='G'/%3E%3C/filter%3E%3ClinearGradient id='g' gradientUnits='userSpaceOnUse' x1='0' y1='24' x2='0' y2='44'%3E%3Cstop offset='0' stop-color='%23fff' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23fff' stop-opacity='1'/%3E%3C/linearGradient%3E%3Crect x='-60' y='-60' width='220' height='220' fill='url(%23g)' filter='url(%23d)'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cfilter id='d' x='-50%25' y='-50%25' width='200%25' height='200%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.007 0.026' numOctaves='4' seed='11' result='n'/%3E%3CfeDisplacementMap in='SourceGraphic' in2='n' scale='24' xChannelSelector='R' yChannelSelector='G'/%3E%3C/filter%3E%3ClinearGradient id='g' gradientUnits='userSpaceOnUse' x1='0' y1='24' x2='0' y2='44'%3E%3Cstop offset='0' stop-color='%23fff' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23fff' stop-opacity='1'/%3E%3C/linearGradient%3E%3Crect x='-60' y='-60' width='220' height='220' fill='url(%23g)' filter='url(%23d)'/%3E%3C/svg%3E");
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}
