/*
 * The portal's look: the shared theme (styles.css, byte-identical with corduroy's and
 * checked by a test, so never edited for this) with the tokens re-set to a coastal palette.
 *
 * Loaded after styles.css, and every rule is scoped to `html.portal`, so nothing here can
 * reach the shop. Light is a sunny beach: sand ground, white surfaces, sea-teal accent, deep
 * water for headings. Dark is the same beach at night: deep-water ground, a paler aqua accent
 * so it still reads on it, sand kept for the brand. The semantic colors (success, danger,
 * warning) are Bootstrap's own on both, as in the shop -- green still means "OK".
 *
 * The mascot is a seagull, still to be drawn. `.portal-mascot` beside the brand is where it
 * goes (see base.html); until then the brand is the word alone.
 */

html.portal,
html.portal[data-bs-theme="light"] {
    --bs-body-bg: #fbf7ef;                 /* dry sand */
    --bs-body-bg-rgb: 251, 247, 239;
    --bs-body-color: #1e3542;              /* deep water, for reading */
    --bs-body-color-rgb: 30, 53, 66;
    --bs-emphasis-color: #0f2a3a;
    --bs-emphasis-color-rgb: 15, 42, 58;
    --bs-secondary-color: #4f6573;         /* muted text: 5.2:1 or better on sand, foam and white */
    --bs-secondary-color-rgb: 79, 101, 115;
    --bs-tertiary-color: #8b9dab;
    --bs-tertiary-color-rgb: 139, 157, 171;
    --bs-secondary-bg: #f3ecdf;
    --bs-secondary-bg-rgb: 243, 236, 223;
    --bs-tertiary-bg: #f7f1e6;
    --bs-tertiary-bg-rgb: 247, 241, 230;
    --bs-border-color: #e6dcc9;            /* a wet-sand line */
    --bs-border-color-translucent: rgba(15, 42, 58, 0.1);

    /* Text teal (links, primary buttons): 5.3:1 or better on white, sand and foam (WCAG AA: 4.5).
       The brighter --coast-sea (3.82:1 on white) is only for drawing. portal/tests/test_contrast.py holds the line. */
    --bs-primary: #0a6d80;                 /* deep sea teal */
    --bs-primary-rgb: 10, 109, 128;
    --bs-primary-bg-subtle: #dff5f7;       /* sea foam */
    --bs-primary-border-subtle: #a9dfe6;
    --bs-primary-text-emphasis: #085868;
    --bs-link-color: #0a6d80;
    --bs-link-color-rgb: 10, 109, 128;
    --bs-link-hover-color: #085868;
    --bs-link-hover-color-rgb: 8, 88, 104;
    --bs-focus-ring-color: rgba(14, 143, 166, 0.3);

    --surface-1: #ffffff;
    --surface-2: #f3ecdf;
    --surface-3: #e9dfcc;
    --accent: var(--bs-primary);
    --accent-hover: #085f70;
    --accent-active: #074f5d;
    --accent-contrast: #ffffff;

    /* The portal's own: the sunny bits. */
    --coast-sun: #f6b73c;                  /* a warm yellow, for small cheer */
    --coast-coral: #ff7a59;
    --coast-foam: #dff5f7;
    --coast-sand: #f1e3c4;
    --coast-sea: #0e8fa6;                  /* the bright sea teal: drawn, never read */
}

html.portal[data-bs-theme="dark"] {
    --bs-body-bg: #0b1a24;                 /* deep water at night */
    --bs-body-bg-rgb: 11, 26, 36;
    --bs-body-color: #d5e2e8;
    --bs-body-color-rgb: 213, 226, 232;
    --bs-emphasis-color: #f1f7f9;
    --bs-emphasis-color-rgb: 241, 247, 249;
    --bs-secondary-color: #8fa6b3;
    --bs-secondary-color-rgb: 143, 166, 179;
    --bs-tertiary-color: #6c8492;
    --bs-tertiary-color-rgb: 108, 132, 146;
    --bs-secondary-bg: #142a38;
    --bs-secondary-bg-rgb: 20, 42, 56;
    --bs-tertiary-bg: #10222e;
    --bs-tertiary-bg-rgb: 16, 34, 46;
    --bs-border-color: #22394a;
    --bs-border-color-translucent: rgba(241, 247, 249, 0.1);

    --bs-primary: #5ed3e6;                 /* aqua, bright enough on deep water */
    --bs-primary-rgb: 94, 211, 230;
    --bs-primary-bg-subtle: #0f3541;
    --bs-primary-border-subtle: #1e5a6b;
    --bs-primary-text-emphasis: #9fe6f1;
    --bs-link-color: #5ed3e6;
    --bs-link-color-rgb: 94, 211, 230;
    --bs-link-hover-color: #8be0ee;
    --bs-link-hover-color-rgb: 139, 224, 238;
    --bs-focus-ring-color: rgba(94, 211, 230, 0.35);

    --surface-1: #12283a;
    --surface-2: #173042;
    --surface-3: #1d3a4d;
    --accent-hover: #7cdbeb;
    --accent-active: #4cc4d8;
    --accent-contrast: #0b1a24;

    --coast-sun: #f6c35c;
    --coast-coral: #ff8f73;
    --coast-foam: #0f3541;
    --coast-sand: #e6d3a8;
    --coast-sea: #5ed3e6;
}

/* ─── Chrome ──────────────────────────────────────────────────────────────── */

/* The navbar is a band of sea foam over the sand, with a thin sun-and-sea stripe under it:
   the one flourish, so the page is cheerful without any art on it yet. */
html.portal .app-nav {
    background-color: var(--coast-foam);
    border-bottom: 0;
}
html.portal .app-nav::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 3px;
    background: linear-gradient(90deg, var(--coast-sun) 0 33%, var(--coast-coral) 33% 66%,
                                var(--coast-sea) 66% 100%);
}
html.portal .app-nav { position: relative; }

html.portal .portal-brand {
    display: inline-flex; align-items: center; gap: 0.5rem;
    font-weight: 800; letter-spacing: 0.01em;
    color: var(--bs-emphasis-color);
}
/* Where the seagull goes: `<img class="portal-mascot" ...>` inside .portal-brand. */
html.portal .portal-mascot { height: 1.9rem; width: auto; display: block; }

/* The footer stays at the bottom of the window on a page shorter than it (sign in, asking for
   an account, the 404) instead of following the form up into the middle of the screen: the
   body is a column at least the window's height and the main part takes what is left over.
   A longer page pushes the footer down as before. dvh where the browser has it, so a phone's
   collapsing address bar does not leave it hanging a bar's height above the bottom. */
html.portal body { display: flex; flex-direction: column; min-height: 100vh; min-height: 100dvh; }
html.portal .portal-main { flex: 1 0 auto; padding-top: 1.25rem; padding-bottom: 2.5rem; }
html.portal .portal-footer { flex-shrink: 0; padding: 1rem 1.25rem 2rem; }

html.portal .card { border-color: var(--bs-border-color); }
html.portal .card-header { background-color: var(--coast-foam); }

html.portal .btn-primary,
html.portal .btn-primary:hover,
html.portal .btn-primary:focus-visible { color: var(--accent-contrast); }

/* ─── The landing page ────────────────────────────────────────────────────── */

/* The landing page is the beach, below; it has no width of its own. */

/* ─── The design page's board preview ─────────────────────────────────────── */

html.portal .board-preview { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1rem; }
html.portal .board-preview .preview-stage { width: 100%; aspect-ratio: 4 / 3; max-width: 100%; display: block; }
html.portal .board-preview .preview-status { font-size: 0.8rem; }

/* `--board-ground` -- the gray a board is drawn on, here and in the popup -- now lives in
   gentoo.css beside the stage that uses it, because the shop's viewer and this one draw the
   same boards and were drawing them on different grounds. The reasoning for the color is
   there; this file just uses it. */

html.portal .board-preview .preview-side { cursor: zoom-in; }
html.portal .board-preview .preview-side:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; border-radius: 0.375rem; }
html.portal .board-preview .preview-frame {
    background: var(--board-ground);
    border-radius: 0.375rem;
    overflow: hidden;
}
/* The pictures are boarddd/view2d stages (fab/flat.js); the holes' rims are drawn over them in
   millimeters, at a constant width on screen (fab-drill styles from gentoo.css). */

/* ─── The enlarged board (a modal) ───────────────────────────────────────── */

html.portal .board-zoom {
    position: relative;
    height: min(80vh, 56rem);
    overflow: hidden;
    background: var(--board-ground);
    cursor: grab;
    touch-action: none;
    border-radius: 0 0 var(--bs-modal-border-radius, 0.5rem) var(--bs-modal-border-radius, 0.5rem);
}
html.portal .board-zoom-inner { position: absolute; inset: 0; }
html.portal .board-zoom-status {
    position: absolute;
    left: 0.6rem;
    bottom: 0.5rem;
    color: var(--bs-secondary-color);
    background: rgba(var(--bs-body-bg-rgb), 0.85);
    padding: 0.1rem 0.45rem;
    border-radius: 0.25rem;
    pointer-events: none;
}

/* ─── The gerber viewer page ─────────────────────────────────────────────── */
/* The stage itself is styled by gentoo.css (.fab-stage, .view3d-stage, .fab-drill …): the
   same ids and classes as the shop's viewer, so the board looks the same on both faces.
   Only the layout around it is the portal's. */
html.portal .portal-viewer { display: flex; gap: 1rem; align-items: stretch; flex-wrap: wrap; }
html.portal .portal-viewer-panes { flex: 1 1 32rem; min-width: min(22rem, 100%); }
html.portal .portal-viewer-side { flex: 0 1 20rem; min-width: min(17rem, 100%); }
html.portal .portal-viewer-side .card-header { background-color: var(--coast-foam); }

/* ─── The seagull ────────────────────────────────────────────────────────── */
/* Drawn by the shop (portal/static/portal/seagull.svg, a board in its beak). The brand's own
   bird, the loading sign, and the 404. The one standing on the landing page's beach is
   beach_gull.svg, beak empty for the fries. */

html.portal .portal-mascot { height: 2.1rem; width: auto; display: block; margin-right: 0.15rem; }

/* Spinning, because a seagull that has spotted a chip does exactly this. One turn every
   1.3 s, and a little lift on the way round so it reads as a wheel rather than a wobble.
   Anyone who asked for less motion gets a gentle bob instead. */
@keyframes gull-spin {
    from { transform: rotate(0deg) translateY(0); }
    50%  { transform: rotate(180deg) translateY(-6%); }
    to   { transform: rotate(360deg) translateY(0); }
}
@keyframes gull-bob {
    from, to { transform: translateY(0); }
    50%      { transform: translateY(-8%); }
}
html.portal .gull-spinner {
    width: 4rem; height: 4rem; display: block;
    animation: gull-spin 1.3s linear infinite;
    filter: drop-shadow(0 2px 2px rgba(15, 42, 58, 0.25));
}
@media (prefers-reduced-motion: reduce) {
    html.portal .gull-spinner { animation: gull-bob 1.6s ease-in-out infinite; }
}

/* Over the whole page, while an upload is stored. */
html.portal .gull-overlay {
    position: fixed; inset: 0; z-index: 1080;
    display: flex; align-items: center; justify-content: center;
    background: rgba(var(--bs-body-bg-rgb), 0.78);
    backdrop-filter: blur(2px);
}
html.portal .gull-overlay-card {
    display: flex; flex-direction: column; align-items: center; gap: 0.75rem;
    padding: 1.5rem 2rem;
    background: var(--surface-1);
    border: 1px solid var(--bs-border-color);
    border-radius: 0.75rem;
    box-shadow: 0 12px 32px rgba(15, 42, 58, 0.18);
}
html.portal .gull-overlay-card .gull-spinner { width: 5.5rem; height: 5.5rem; }
html.portal .gull-overlay-text { font-weight: 600; color: var(--bs-emphasis-color); }

/* Over one picture, while it is drawn. The parent is positioned (the preview frame, the
   zoom stage, the viewer stage). */
html.portal .gull-loader {
    position: absolute; inset: 0; z-index: 3;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.4rem;
    pointer-events: none;
}
html.portal .gull-loader-text {
    color: var(--bs-emphasis-color);
    background: rgba(var(--bs-body-bg-rgb), 0.85);
    padding: 0.1rem 0.5rem; border-radius: 0.25rem;
}
html.portal .board-preview .preview-frame { position: relative; }
/* The viewer stage is dark; the caption keeps its light pill so it reads on it. */
html.portal .fab-stage .gull-loader-text,
html.portal .view3d-stage .gull-loader-text { color: #fff; background: rgba(0, 0, 0, 0.45); }

/* ─── The landing page: a beach ──────────────────────────────────────────── */
/* Sky, sea, sand, the sun (the moon after dark), a sailboat now and then, the gull, and a
   carton of fries it has its eye on. It fills the window below the navbar; the words are
   the user's. beach.js moves the loose fries, the gull and the boat; everything else is
   these rules and inline SVG. */

html.portal {
    --beach-sky-top: #bfe9f7;
    --beach-sky-bottom: #e8f7fb;
    --beach-sun: #f6b73c;
    --beach-sea: #1aa3bc;
    --beach-sea-deep: #0e8fa6;
    --beach-foam: rgba(255, 255, 255, 0.85);
    --beach-sand: #f1e3c4;
    --beach-sand-wet: #e2cfa4;
    --beach-boat-hull: #23384a;
    --beach-boat-sail: #fdfdfb;
    --beach-boat-sail-edge: #b9c9d1;
    --beach-ship-hull: #3a4a58;
    --beach-ship-boot: #8b3a30;
    --beach-ship-house: #e9edf0;
    --beach-ship-funnel: #c9473a;
    --beach-ship-window: #9fc3d8;
    --beach-plane-body: #f6f8f9;
    --beach-plane-belly: #c9d4db;
    --beach-plane-tail: #0e8fa6;
    --beach-plane-wing: #d7e0e5;
    --beach-plane-engine: #a9b8c2;
    --beach-plane-intake: #5c6f7c;
    --beach-plane-window: #3c4f5d;
    --beach-plane-cockpit: #3c4f5d;
}
html.portal[data-bs-theme="dark"] {
    --beach-sky-top: #0b1a24;
    --beach-sky-bottom: #12303f;
    --beach-sun: #f1ead6;              /* the moon */
    --beach-sea: #145a6b;
    --beach-sea-deep: #0f3541;
    --beach-foam: rgba(214, 240, 246, 0.55);
    --beach-sand: #6f5f44;
    --beach-sand-wet: #5a4c36;
    --beach-boat-hull: #060f16;
    --beach-boat-sail: #6e848f;
    --beach-boat-sail-edge: #4c6470;
    --beach-ship-hull: #07121a;
    --beach-ship-boot: #24191a;
    --beach-ship-house: #2a3d48;
    --beach-ship-funnel: #3a2a2c;
    --beach-ship-window: #ffd98a;          /* lit */
    --beach-plane-body: #33434f;
    --beach-plane-belly: #1b262e;
    --beach-plane-tail: #135866;
    --beach-plane-wing: #26343e;
    --beach-plane-engine: #17222a;
    --beach-plane-intake: #0d151a;
    --beach-plane-window: #ffd98a;         /* lit */
    --beach-plane-cockpit: #0e161c;        /* not: the pilots keep their lights off */
}

/* The landing page's main has no gutter: the beach runs edge to edge and down to the
   bottom of the window (the navbar is about 3.4rem tall; the footer sits below the fold). */
html.portal .portal-main.is-beach { padding: 0; }
html.portal .beach {
    position: relative;
    overflow: hidden;
    margin: 0;
    height: calc(100vh - 3.4rem);
    height: calc(100dvh - 3.4rem);
    min-height: 30rem;
    background: linear-gradient(to bottom, var(--beach-sky-top), var(--beach-sky-bottom) 60%);
    user-select: none;
}
html.portal .beach-sun {
    position: absolute; top: 8%; right: 10%;
    width: 7rem; height: 7rem; border-radius: 50%;
    background: var(--beach-sun);
    box-shadow: 0 0 60px 18px color-mix(in srgb, var(--beach-sun) 45%, transparent);
}
/* The sand is the bottom 28% of the beach and the sea the 18% above it -- beach.js puts
   the fries down at 88.5% of the height and the gull's feet just under that. The sea is
   flat to the horizon, as open water is from a beach, deeper in color far off; three faint
   lines of swell run in toward the shore. The breaking is at the shoreline (.beach-surf). */
html.portal .beach-sea {
    position: absolute; left: 0; right: 0; bottom: 28%; height: 18%;
    background: linear-gradient(to bottom, var(--beach-sea-deep), var(--beach-sea) 40%);
    overflow: hidden;
}
html.portal .beach-swell {
    position: absolute; left: 0; right: 0; top: 30%; height: 2px;
    background: var(--beach-foam);
    opacity: 0;
    animation: swell-run 9s linear infinite;
}
html.portal .beach-swell:nth-child(2) { animation-delay: -3s; }
html.portal .beach-swell:nth-child(3) { animation-delay: -6s; }
@keyframes swell-run {
    from { top: 30%; opacity: 0; }
    40%  { opacity: 0.35; }
    to   { top: 100%; opacity: 0.6; }
}
/* The vessels, far out: a container ship and a sailboat. The horizon is the top of the sea,
   46% up. The sailboat sits in front of the swell with its keel a hair below the horizon.
   The ship is further out, behind the waves, which lap along its hull as they drift; its
   keel is below the horizon so no sky shows under it in a trough, and its hull (32% of the
   drawing's height) is taller than the highest crest -- the front wave is 1.8rem tall with
   its top 1.6rem above the sea and its crest at 12/40 of the way down, so 1.06rem -- at any
   width the clamp allows above a phone. beach.js sets `left` and flips each to face the way
   it sails; with no script they sit here. The boat rocks about its waterline; the ship does
   not. */
html.portal .beach-vessel {
    position: absolute; height: auto;
    will-change: left;
}
html.portal .beach-boat {
    left: 30%; bottom: calc(46% - 0.15rem);
    width: clamp(2.2rem, 3.4vw, 3.6rem);
    transform-origin: 50% 96%;
    animation: boat-bob 5s ease-in-out infinite;
}
@keyframes boat-bob {
    from, to { rotate: -1.5deg; translate: 0 0; }
    50%      { rotate: 1.5deg; translate: 0 0.06rem; }
}
html.portal .beach-boat .boat-hull { fill: var(--beach-boat-hull); }
html.portal .beach-boat .boat-sail { fill: var(--beach-boat-sail); stroke: var(--beach-boat-sail-edge); stroke-width: 1; stroke-linejoin: round; }
html.portal .beach-boat .boat-mast { fill: none; stroke: var(--beach-boat-hull); stroke-width: 2; stroke-linecap: round; }
html.portal .beach-ship {
    left: 65%; bottom: calc(46% - 0.3rem);
    width: clamp(8rem, 13vw, 15rem);
}
html.portal .beach-ship .ship-hull { fill: var(--beach-ship-hull); }
html.portal .beach-ship .ship-boot { fill: var(--beach-ship-boot); }
html.portal .beach-ship .ship-house { fill: var(--beach-ship-house); }
html.portal .beach-ship .ship-funnel { fill: var(--beach-ship-funnel); }
html.portal .beach-ship .ship-window { fill: var(--beach-ship-window); }
html.portal .beach-ship .ship-mast { fill: none; stroke: var(--beach-ship-hull); stroke-width: 1.5; stroke-linecap: round; }
html.portal[data-bs-theme="dark"] .beach-ship .ship-boxes { filter: brightness(0.35) saturate(0.6); }
/* Navigation lights, after dark only. Sailing right the beach sees the starboard side and
   its green light; flipped to sail left, the port side and red. The stern light is white
   whichever way, as are the ship's two masthead lights. */
html.portal .beach-vessel .nav-light { display: none; fill: currentColor; }
html.portal .beach-vessel .nav-light .halo { opacity: 0.35; }
html.portal[data-bs-theme="dark"] .beach-vessel .nav-light { display: inline; }
html.portal .beach-vessel .nav-light.is-side { color: #2ee87a; }
html.portal .beach-vessel.is-sailing-left .nav-light.is-side { color: #ff3b3b; }
html.portal .beach-vessel .nav-light.is-stern, html.portal .beach-vessel .nav-light.is-masthead { color: #fff; }
html.portal .beach-sand {
    position: absolute; left: 0; right: 0; bottom: 0; height: 28%;
    background: linear-gradient(to bottom, var(--beach-sand-wet), var(--beach-sand) 22%);
}
/* The surf, where the water meets the sand: two sets of white water that run up the beach
   and drain back (surf-surge, on the foam), drifting sideways as they go (surf-drift, on the
   set: a strip twice the beach's width moved by half of itself, which is why the foam's edge
   repeats every half), one behind the other half a cycle apart so there is always a wave
   breaking. The wet sand at the top of the beach follows the wash a moment later.
   `--surf-surge`, on the beach, is how far up the sand the wash runs: 1 on a calm day;
   weather.js raises it with the wind. */
html.portal .beach { --surf-surge: 1; }
html.portal .beach-surf {
    position: absolute; left: 0; right: 0; bottom: calc(28% - 1.6rem); height: 3.2rem;
    pointer-events: none;
}
html.portal .surf-set {
    position: absolute; left: 0; top: 0; width: 200%; height: 100%;
    animation: surf-drift 14s linear infinite;
}
html.portal .surf-set.is-back { animation-direction: reverse; animation-duration: 19s; }
html.portal .surf-foam {
    display: block; width: 100%; height: 100%;
    fill: var(--beach-foam);
    filter: blur(0.6px);
    transform-origin: 50% 0;
    animation: surf-surge 7s ease-in-out infinite;
}
html.portal .surf-set.is-back .surf-foam { opacity: 0.6; animation-delay: -3.5s; }
@keyframes surf-drift { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes surf-surge {
    from, to { transform: translateY(calc(-0.4rem * var(--surf-surge))) scaleY(0.55); opacity: 0.55; }
    50%      { transform: translateY(calc(1.1rem * var(--surf-surge))) scaleY(1); opacity: 1; }
}
html.portal .beach-wet {
    position: absolute; left: 0; right: 0; top: 0; height: 2.4rem;
    background: linear-gradient(to bottom, rgba(20, 50, 70, 0.22), rgba(20, 50, 70, 0));
    animation: wet-surge 7s ease-in-out infinite;
    animation-delay: 0.5s;
}
@keyframes wet-surge {
    from, to { transform: translateY(0); opacity: 0.55; }
    50%      { transform: translateY(calc(0.9rem * var(--surf-surge))); opacity: 1; }
}
/* Still while idle; beach.js sets `left` and flips it to face the way it walks. In front
   of the carton (z-index 2 to its 1); a loose fry lies in front of both (3). Its shadow is
   on the sand under its feet, moved along with it by beach.js.
   The feet stand at 9% up the beach and the shadow is centered on that line, at every width.
   beach_gull.svg has a strip of empty canvas under the feet, 4.1% of its height (3.5% of its
   width, at 788 x 674), so the drawing is let down by that much; measured on the drawing, so
   it is to be measured again if the drawing changes. Before 2026-10-03 the box's bottom stood
   on the line and the bird floated above its shadow (internal#99). */
html.portal .beach {
    --gull-w: clamp(7rem, 15vw, 15rem);
    --gull-shadow-h: 1.3rem;
}
html.portal .beach-gull-shadow {
    position: absolute; bottom: calc(9% - var(--gull-shadow-h) / 2); left: 62%;
    width: clamp(5rem, 10.5vw, 10.5rem); height: var(--gull-shadow-h);
    border-radius: 50%;
    background: radial-gradient(ellipse at center, rgba(15, 42, 58, 0.38), rgba(15, 42, 58, 0) 72%);
    z-index: 1;
    will-change: left;
}
html.portal .beach-gull {
    position: absolute; bottom: calc(9% - var(--gull-w) * 0.035); left: 60%;
    width: var(--gull-w); height: auto;
    z-index: 2;
    filter: drop-shadow(0 2px 2px rgba(15, 42, 58, 0.15));
    transform-origin: 50% 90%;
    /* beach.js turns the gull round with scaleX(-1); passing through zero reads as a turn. */
    transition: transform 0.22s ease-in-out;
    will-change: left, transform;
}
@keyframes gull-waddle {
    from { rotate: -4deg; translate: 0 0; }
    50%  { rotate: 4deg; translate: 0 -0.35rem; }
    to   { rotate: -4deg; translate: 0 0; }
}
html.portal .beach-gull.is-waddling { animation: gull-waddle 0.34s ease-in-out infinite; }
html.portal .beach-fries {
    position: absolute; bottom: 10.5%; right: 7%;
    width: clamp(3rem, 5.5vw, 6rem); height: auto;
    z-index: 1;
    filter: drop-shadow(0 4px 3px rgba(15, 42, 58, 0.25));
}
/* A loose fry: positioned by beach.js in the beach's own pixels, grabbable. */
html.portal .beach-fry {
    position: absolute; left: 0; top: 0;
    width: 0.6rem; height: 2.4rem;
    cursor: grab;
    touch-action: none;
    filter: drop-shadow(0 2px 2px rgba(15, 42, 58, 0.3));
    z-index: 3;
}
html.portal .beach-fry.is-held { cursor: grabbing; filter: drop-shadow(0 8px 6px rgba(15, 42, 58, 0.35)); z-index: 4; }
/* Being eaten: beach.js flies it into the beak and shrinks it there, all in its transform.
   (Not the `scale` property: that applies outside the translate, so it shrinks the fry's
   position toward the beach's top-left corner along with the fry.) */
html.portal .beach-fry.is-eaten { pointer-events: none; }
html.portal .beach-fry.is-static { top: auto; bottom: 10%; transform: rotate(-35deg); }
/* An aircraft over the bay: a link to its page on the aggregator, positioned, flipped and
   pitched by sky.js in its transform (nose to the right as drawn; `is-flying-left` when
   flipped), shown (`is-shown`) while it is in view. Behind the words, in front of the sky
   and the sun. The gear comes down below 2500 ft (`is-low`). After dark it shows its
   lights: the red beacon pulsing, the white strobe flashing, the wingtip light on the side
   the beach sees -- green to starboard flying right, red to port flying left, as the
   vessels' -- and a landing light with the gear. */
html.portal .beach-plane {
    position: absolute; left: 0; top: 0; display: block;
    width: clamp(3.5rem, 6.5vw, 7.5rem); height: auto;   /* small, as an airliner miles off is */
    z-index: 1;                 /* under the words (2), whatever else: they are the page */
    transform-origin: 50% 50%;
    opacity: 0;
    transition: opacity 0.6s ease;
    pointer-events: none;
    filter: drop-shadow(0 2px 2px rgba(15, 42, 58, 0.2));
    will-change: transform;
}
html.portal .beach-plane.is-shown { opacity: 1; pointer-events: auto; cursor: pointer; }
html.portal .beach-plane svg { display: block; width: 100%; height: auto; }
html.portal .beach-plane .plane-body { fill: var(--beach-plane-body); }
html.portal .beach-plane .plane-belly { fill: var(--beach-plane-belly); }
html.portal .beach-plane .plane-cheat { stroke: var(--beach-plane-intake); }
html.portal .beach-plane .plane-door { stroke: var(--beach-plane-intake); }
html.portal .beach-plane .plane-tail { fill: var(--beach-plane-tail); }
html.portal .beach-plane .plane-rudder { fill: #000; }
html.portal .beach-plane .plane-stab, html.portal .beach-plane .plane-wing { fill: var(--beach-plane-belly); }   /* edge-on: the underside */
html.portal .beach-plane .plane-winglet { fill: var(--beach-plane-engine); }
html.portal .beach-plane .plane-engine, html.portal .beach-plane .plane-pylon { fill: var(--beach-plane-engine); }
html.portal .beach-plane .plane-intake, html.portal .beach-plane .plane-exhaust { fill: var(--beach-plane-intake); }
html.portal .beach-plane .plane-glass { fill: var(--beach-plane-window); }
html.portal .beach-plane .plane-cockpit { fill: var(--beach-plane-cockpit); }
html.portal .beach-plane .plane-gear { display: none; }
html.portal .beach-plane.is-low .plane-gear { display: inline; }
html.portal .beach-plane .plane-strut { stroke: var(--beach-plane-intake); }
html.portal .beach-plane .plane-wheel { fill: #22282c; }
html.portal .beach-plane .nav-light { display: none; fill: currentColor; }
html.portal .beach-plane .nav-light .halo { opacity: 0.35; }
html.portal[data-bs-theme="dark"] .beach-plane .nav-light { display: inline; }
html.portal[data-bs-theme="dark"] .beach-plane .nav-light.is-landing { display: none; }
html.portal[data-bs-theme="dark"] .beach-plane.is-low .nav-light.is-landing { display: inline; }
html.portal .beach-plane .nav-light.is-beacon { color: #ff3b3b; animation: plane-beacon 1.2s ease-in-out infinite; }
html.portal .beach-plane .nav-light.is-strobe { color: #fff; animation: plane-strobe 1.6s steps(1) infinite; }
html.portal .beach-plane .nav-light.is-side { color: #2ee87a; }
html.portal .beach-plane.is-flying-left .nav-light.is-side { color: #ff3b3b; }
html.portal .beach-plane .nav-light.is-landing { color: #fff8e0; }
@keyframes plane-beacon { from, to { opacity: 0.15; } 50% { opacity: 1; } }
@keyframes plane-strobe { from, 4% { opacity: 1; } 5%, to { opacity: 0; } }
/* ─── The weather ────────────────────────────────────────────────────────── */
/* weather.js reads the sky over SFO (settings.PORTAL_WEATHER_FEED, Open-Meteo) and sets
   these on the beach: a cloud for every so much cloud cover, drifting with the wind
   (weather.js sets each one's place, size, and how long it takes to cross); `is-overcast`
   (the sky grays, the sun goes); `is-foggy` (a fog bank on the water, and what is far off
   -- the vessels, the aircraft -- fades into it); `is-raining` (streaks slanted by the
   wind, `--rain` for how heavy). Nothing here without the script: the beach is fair. */
html.portal {
    --beach-cloud: #ffffff;
    --beach-cloud-shade: #d3dee5;
    --beach-deck: rgba(150, 165, 175, 0.55);
    --beach-fog: rgba(226, 234, 238, 0.94);
    --beach-rain: rgba(255, 255, 255, 0.55);
}
html.portal[data-bs-theme="dark"] {
    --beach-cloud: #233440;
    --beach-cloud-shade: #172530;
    --beach-deck: rgba(20, 32, 40, 0.6);
    --beach-fog: rgba(34, 48, 58, 0.94);
    --beach-rain: rgba(200, 220, 230, 0.35);
}
html.portal .beach.is-overcast, html.portal .beach.is-raining {
    --beach-sky-top: #a7b6bf; --beach-sky-bottom: #d5dde1;
    --beach-sea: #5d8b9a; --beach-sea-deep: #4a7482;
}
html.portal[data-bs-theme="dark"] .beach.is-overcast, html.portal[data-bs-theme="dark"] .beach.is-raining {
    --beach-sky-top: #0a1218; --beach-sky-bottom: #17242c;
    --beach-sea: #123f4c; --beach-sea-deep: #0d2c36;
}
html.portal .beach-sun { transition: opacity 1.5s ease; }
html.portal .beach.is-overcast .beach-sun, html.portal .beach.is-raining .beach-sun { opacity: 0; }
html.portal .beach.is-foggy .beach-sun { opacity: 0.35; box-shadow: none; }
html.portal .beach-clouds {
    position: absolute; left: 0; right: 0; top: 0; bottom: 46%;
    overflow: hidden; pointer-events: none;
}
html.portal .beach.is-overcast .beach-clouds::before {
    content: ''; position: absolute; left: 0; right: 0; top: 0; height: 100%;
    background: linear-gradient(to bottom, var(--beach-deck), transparent 70%);
}
html.portal .beach-cloud {
    position: absolute; left: 0; top: 10%;
    width: 12rem; height: auto;
    fill: var(--beach-cloud);
    animation: cloud-drift 120s linear infinite;
}
html.portal .beach-cloud .cloud-shade { fill: var(--beach-cloud-shade); }
html.portal .beach-cloud.is-westward { animation-name: cloud-drift-west; }
html.portal .beach-cloud.is-still { animation: none; }
@keyframes cloud-drift { from { transform: translateX(-110%); } to { transform: translateX(100vw); } }
@keyframes cloud-drift-west { from { transform: translateX(100vw); } to { transform: translateX(-110%); } }
html.portal .beach-fog {
    position: absolute; left: 0; right: 0; top: 0; bottom: 20%;
    background: linear-gradient(to bottom, transparent 0%, var(--beach-fog) 45%, var(--beach-fog) 78%, transparent 100%);
    opacity: 0; transition: opacity 2s ease;
    pointer-events: none;
}
html.portal .beach.is-foggy .beach-fog { opacity: 1; }
html.portal .beach.is-foggy .beach-vessel { opacity: 0.2; }
html.portal .beach.is-foggy .beach-plane.is-shown { opacity: 0.3; }
html.portal .beach.is-overcast .beach-plane.is-shown { opacity: 0.85; }
html.portal .beach-rain {
    position: absolute; left: -25%; right: -25%; top: -25%; bottom: 20%;
    --rain: 0.6; --rain-angle: 0deg;
    transform: rotate(var(--rain-angle));
    /* Two runs of streaks with different spacings, so the drops do not line up in a grid;
       the mask cuts the bands into thin threads. */
    background-image:
        repeating-linear-gradient(to bottom, transparent 0 22px, var(--beach-rain) 22px 32px, transparent 32px 48px),
        repeating-linear-gradient(to bottom, transparent 0 40px, var(--beach-rain) 40px 52px, transparent 52px 73px);
    background-size: 100% 48px, 100% 73px;
    background-position: 0 0, 7px 0;
    -webkit-mask-image: repeating-linear-gradient(to right, #000 0 1.5px, transparent 1.5px 13px, #000 13px 14.5px, transparent 14.5px 31px);
    mask-image: repeating-linear-gradient(to right, #000 0 1.5px, transparent 1.5px 13px, #000 13px 14.5px, transparent 14.5px 31px);
    opacity: 0; transition: opacity 1.5s ease;
    animation: rain-fall 0.45s linear infinite;
    pointer-events: none;
}
html.portal .beach.is-raining .beach-rain { opacity: var(--rain); }
@keyframes rain-fall { from { background-position: 0 0, 7px 0; } to { background-position: 0 48px, 7px 73px; } }
@media (prefers-reduced-motion: reduce) {
    html.portal .beach-plane { transition: none; }
    html.portal .beach-plane .nav-light.is-beacon, html.portal .beach-plane .nav-light.is-strobe { animation: none; }
    html.portal .beach-boat { animation: none; }
    html.portal .beach-swell { animation: none; opacity: 0.3; top: 60%; }
    html.portal .surf-set, html.portal .surf-foam, html.portal .beach-wet { animation: none; }
    html.portal .beach-cloud { animation: none; }
    html.portal .beach-rain { display: none; }
    html.portal .beach-gull { transition: none; }
    html.portal .beach-gull.is-waddling { animation: none; }
}
/* The words, over the sky on the left. The block is a good part of the sky and the
   aircraft fly behind it, so the pointer passes through it to them; only its buttons
   catch the pointer themselves. (The beach is user-select: none, so there is no text to
   select through it either.) */
html.portal .beach-words {
    position: relative; z-index: 2;
    max-width: 34rem;
    padding: clamp(2rem, 8vh, 6rem) clamp(1.25rem, 5vw, 4rem) 2rem;
    pointer-events: none;
}
html.portal .beach-words a, html.portal .beach-words button { pointer-events: auto; }
html.portal .beach-words h1 { font-size: clamp(2rem, 4.5vw, 3.4rem); font-weight: 800; line-height: 1.1; margin-bottom: 0.6rem; }
html.portal .beach-words p { font-size: clamp(1rem, 1.4vw, 1.2rem); }
@media (max-width: 640px) {
    html.portal .beach-sun { width: 4rem; height: 4rem; top: 1rem; right: 1.5rem; }
}

/* The 404: nothing here but a gull that got the fry. */
html.portal .lost-gull { width: 7rem; height: auto; transform: scaleX(-1); }

/* ─── The BOM page ───────────────────────────────────────────────────────── */
html.portal .bom-table td { vertical-align: top; }
html.portal .bom-part-image {
    width: 3rem; height: 3rem; object-fit: contain;
    background: #fff; border: 1px solid var(--bs-border-color); border-radius: 0.25rem;
    flex: 0 0 auto;
}

/* The wait, in the row or card that asked: a small gull, a sentence, a bar that slides. */
html.portal .gull-inline { display: flex; gap: 0.6rem; align-items: center; padding: 0.25rem 0; }
html.portal .gull-inline .gull-spinner { width: 2.2rem; height: 2.2rem; flex: 0 0 auto; }
html.portal .gull-inline-body { flex: 1 1 auto; min-width: 10rem; }
html.portal .gull-inline-track { height: 0.35rem; margin-top: 0.3rem; border-radius: 0.2rem; overflow: hidden; background: var(--bs-secondary-bg); }
html.portal .gull-inline-fill { width: 35%; height: 100%; border-radius: inherit; background: var(--bs-primary); animation: gull-slide 1.1s ease-in-out infinite; }
@keyframes gull-slide { from { transform: translateX(-100%); } to { transform: translateX(300%); } }
@media (prefers-reduced-motion: reduce) { html.portal .gull-inline-fill { animation: none; width: 100%; opacity: 0.5; } }
html.portal .bom-options li { font-size: 0.8rem; }

/* ─── The BOM on the BOM step: the lines that need you first ── */
html.portal .bom-chip {
    display: inline-flex; align-items: center; gap: 0.35rem;
    padding: 0.2rem 0.6rem; min-height: 2rem; border-radius: 1rem;
    border: 1px solid var(--bs-border-color); background: var(--bs-body-bg);
    color: var(--bs-body-color); font-size: 0.85rem; text-decoration: none; white-space: nowrap;
}
html.portal a.bom-chip:hover { border-color: var(--bs-primary); }
html.portal .bom-chip-answer { border-color: #d98a00; background: #fff4d6; color: #5c3b00; }
html.portal .bom-chip-answer i { color: #d98a00; font-size: 0.6rem; }
html.portal .bom-chip-look { border-color: #e0b84a; }
html.portal .bom-chip-look i { color: #b97a00; }
html.portal .bom-chip-ok i { color: var(--bs-success); }
html.portal .bom-boards { width: 5.5rem; }
html.portal .bom-allclear { color: var(--bs-success-text-emphasis); }

html.portal .bom-issue {
    display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center;
    padding: 0.6rem 0.75rem; margin-bottom: 0.4rem;
    border: 1px solid var(--bs-border-color); border-left-width: 0.3rem; border-radius: 0.4rem;
    background: var(--bs-body-bg);
}
html.portal .bom-issue-answer { border-left-color: #d98a00; }
html.portal .bom-issue-look { border-left-color: #e0b84a; }
html.portal .bom-issue-what { flex: 1 1 16rem; min-width: 0; }
html.portal .bom-issue-do { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: stretch; }
html.portal .bom-issue-do form { display: contents; }
html.portal .bom-issue-do .btn { min-height: 2.75rem; }
html.portal .bom-use { display: inline-flex; flex-direction: column; align-items: flex-start; line-height: 1.15; text-align: left; }
html.portal .bom-use-name { font-weight: 600; }
html.portal .bom-use-facts { font-size: 0.75rem; opacity: 0.9; }
html.portal .bom-find { min-width: 2.75rem; }
html.portal .bom-badge-looking { display: inline-flex; align-items: center; gap: 0.3rem; background: var(--bs-body-bg); color: var(--bs-secondary-color); border: 1px dashed var(--bs-border-color); }
html.portal .bom-badge-looking .gull-spinner { width: 1rem; height: 1rem; filter: none; }
html.portal .bom-others { flex: 1 1 100%; }
html.portal .bom-others > summary { cursor: pointer; color: var(--bs-secondary-color); }
html.portal .bom-row .bom-part-image, html.portal .bom-others .bom-part-image, html.portal .bom-issue .bom-part-image { width: 2.75rem; height: 2.75rem; }
html.portal .min-w-0 { min-width: 0; }
html.portal .badge[class*="bom-badge-"] { font-weight: 600; vertical-align: 0.1em; }
html.portal .bom-badge-unmatched, html.portal .bom-badge-ambiguous, html.portal .bom-badge-no_number { background: #fff0c2; color: #5c3b00; border: 1px solid #e0b84a; }
html.portal .bom-badge-out_of_stock, html.portal .bom-badge-long_lead, html.portal .bom-badge-unpriced { background: var(--bs-body-bg); color: #6b4a00; border: 1px solid #e0b84a; }
html.portal .bom-badge-library { background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); }
html.portal .bom-badge-candidate { background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); }

html.portal .bom-row {
    display: grid; grid-template-columns: minmax(0, 1fr) 4.5rem 4rem 3.5rem auto;
    gap: 0.25rem 0.75rem; align-items: center;
    padding: 0.4rem 0.75rem; border-top: 1px solid var(--bs-border-color); font-size: 0.875rem;
}
html.portal .bom-row-head { font-size: 0.75rem; color: var(--bs-secondary-color); border-top: 0; padding-top: 0.3rem; padding-bottom: 0.3rem; }
html.portal .bom-row .num { text-align: right; font-variant-numeric: tabular-nums; }
html.portal .bom-row-none { grid-column: 2 / 5; text-align: right; }
html.portal .bom-row-dnp .bom-row-part { opacity: 0.65; }
html.portal .bom-row-who { display: flex; gap: 0.4rem; align-items: center; justify-content: flex-end; }
html.portal .bom-row-who .btn { min-height: 2.75rem; white-space: nowrap; }
html.portal .bom-row-changed { animation: bom-flash 1.6s ease-out; }
@keyframes bom-flash { from { background: var(--bs-primary-bg-subtle); } to { background: transparent; } }
html.portal .bom-saving { opacity: 0.6; }
@media (max-width: 767.98px) {
    /* Phone: the part on its own line, the three figures labeled under it, the answer below. */
    html.portal .bom-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    html.portal .bom-row-head { display: none; }
    html.portal .bom-row-part, html.portal .bom-row-who { grid-column: 1 / -1; }
    html.portal .bom-row-who { justify-content: flex-start; }
    html.portal .bom-row .num { text-align: left; }
    html.portal .bom-row .num::before { content: attr(data-label) " "; color: var(--bs-secondary-color); font-size: 0.75rem; }
    html.portal .bom-row-none { grid-column: 1 / -1; text-align: left; }
    html.portal .bom-issue-do { width: 100%; }
    html.portal .bom-issue-do .bom-use { flex: 1 1 100%; }
    html.portal .bom-issue-do .btn:not(.bom-use):not(.bom-find) { flex: 1 1 0; }
}
html.portal .bom-toast {
    position: fixed; left: 50%; bottom: 1.25rem; transform: translateX(-50%); z-index: 1090;
    max-width: min(32rem, calc(100vw - 2rem)); padding: 0.5rem 0.9rem; border-radius: 0.5rem;
    background: #17323f; color: #fff; font-size: 0.875rem; box-shadow: 0 0.25rem 1rem rgba(15, 42, 58, 0.3);
}
html.portal .bom-toast-bad { background: #7a2318; }

/* ─── Ordering ───────────────────────────────────────────────────────────── */
/* The steps across the top: where you are, what is done, what is only for assembly, and a
   coral dot where something needs an answer. */
html.portal .order-steps ol { display: flex; flex-wrap: wrap; gap: 0.35rem; list-style: none; margin: 0; padding: 0; }
html.portal .order-step a, html.portal .order-step > span {
    display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.75rem 0.3rem 0.35rem;
    border-radius: 999px; border: 1px solid var(--bs-border-color); background: var(--surface-1);
    color: var(--bs-body-color); text-decoration: none; font-size: 0.875rem; position: relative;
}
html.portal .order-step-n {
    display: inline-grid; place-items: center; width: 1.5rem; height: 1.5rem; border-radius: 999px;
    background: var(--bs-secondary-bg); font-size: 0.75rem; font-weight: 600;
}
html.portal .order-step.is-current a { border-color: var(--accent); box-shadow: 0 0 0 2px var(--bs-focus-ring-color); font-weight: 600; }
html.portal .order-step.is-current .order-step-n { background: var(--accent); color: var(--accent-contrast); }
html.portal .order-step.is-done .order-step-n { background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); }
html.portal .order-step.is-skipped > span { opacity: 0.5; border-style: dashed; cursor: default; }
html.portal .order-step.is-blocked a::after {
    content: ""; position: absolute; top: 0.15rem; right: 0.2rem; width: 0.45rem; height: 0.45rem;
    border-radius: 999px; background: var(--coast-coral);
}

/* The order and its price, pinned beside the step on a wide screen. */
@media (min-width: 992px) {
    html.portal .order-rail { position: sticky; top: 1rem; }
}
html.portal .order-rail.is-updating .order-price { opacity: 0.55; transition: opacity 0.15s; }
html.portal .order-rail-rows > div { display: flex; justify-content: space-between; gap: 0.75rem; }
html.portal .order-rail-rows dt { font-weight: 400; color: var(--bs-secondary-color); }
html.portal .order-rail-rows dd { margin: 0; text-align: right; }
html.portal .order-rail-label { font-size: 0.72rem; letter-spacing: 0.06em; margin-bottom: 0.25rem; }
html.portal .order-price-lines td, html.portal .order-price-lines th { padding: 0.15rem 0; background: transparent; }
html.portal .order-price-lines .num { font-variant-numeric: tabular-nums; }
html.portal .order-price-total th { border-top: 1px solid var(--bs-border-color); padding-top: 0.35rem; font-size: 1rem; }
/* An order of several boards: each board's lines fold under its name and subtotal, the
   order's own (turnaround, delivery, the minimum) follow once. The fold row is set like a line:
   1rem, as the tables resolve to, not the card body's .875em. */
html.portal .order-price-board > summary {
    display: flex; gap: 0.75rem; font-size: 1rem; padding: 0.15rem 0; font-variant-numeric: tabular-nums;
}
html.portal .order-price-board > summary::before { margin-right: -0.5rem; }
html.portal .order-price-board > summary > span:last-child { margin-left: auto; }
html.portal .order-price-board .order-price-lines { margin-left: 0.9rem; width: calc(100% - 0.9rem); }
html.portal .order-review { background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); border-radius: 0.35rem; padding: 0.4rem 0.6rem; }
html.portal .order-checklist { list-style: none; padding: 0; }
html.portal .order-checklist li { position: relative; padding-left: 1.1rem; margin-bottom: 0.25rem; }
html.portal .order-checklist li::before { content: "○"; position: absolute; left: 0; color: var(--coast-coral); }
html.portal .order-ready::before { content: "● "; color: var(--bs-success); }

/* What the files say, as chips. */
html.portal .order-facts { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
html.portal .order-facts span {
    font-size: 0.8rem; padding: 0.15rem 0.55rem; border-radius: 0.3rem;
    background: var(--bs-primary-bg-subtle); color: var(--bs-emphasis-color); font-variant-numeric: tabular-nums;
}
html.portal .order-facts .order-facts-title {
    background: transparent; color: var(--bs-secondary-color); text-transform: uppercase;
    letter-spacing: 0.06em; font-size: 0.7rem; padding-left: 0;
}

/* One option: label on the left, answers on the right, stacking on a narrow screen. */
html.portal .order-option { display: grid; grid-template-columns: 11rem 1fr; gap: 0.25rem 1rem; padding: 0.55rem 0; }
html.portal .order-option + .order-option { border-top: 1px dashed var(--bs-border-color); }
html.portal .order-option[hidden] { display: none; }
@media (max-width: 575.98px) { html.portal .order-option { grid-template-columns: 1fr; } }
html.portal .order-choices { display: flex; flex-wrap: wrap; gap: 0.3rem; }
html.portal .order-choices .btn-check:checked + .btn { background: var(--bs-primary-bg-subtle); color: var(--bs-emphasis-color); border-color: var(--accent); font-weight: 600; }
html.portal .order-choices .btn-check:disabled + .btn { text-decoration: line-through; opacity: 0.45; }
html.portal .order-why:empty { display: none; }
html.portal .order-parts .btn { white-space: nowrap; }
html.portal .order-silk-dot { display: inline-block; width: 0.7rem; height: 0.7rem; border-radius: 50%; margin-right: 0.35rem;
    background: var(--swatch); box-shadow: 0 0 0 1px var(--bs-border-color); vertical-align: -0.05rem; }
html.portal .order-more summary { color: var(--accent); cursor: pointer; }
html.portal .order-swatches { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
html.portal .order-swatch {
    width: 1.6rem; height: 1.6rem; border-radius: 0.3rem; background: var(--swatch);
    border: 2px solid var(--bs-border-color); cursor: pointer; margin: 0;
}
html.portal .btn-check:checked + .order-swatch { border-color: var(--accent); box-shadow: 0 0 0 2px var(--bs-focus-ring-color); }
html.portal .btn-check:focus-visible + .order-swatch { outline: 2px solid var(--accent); outline-offset: 2px; }
html.portal .btn-check:disabled + .order-swatch { opacity: 0.35; cursor: not-allowed; }

/* The Placement step's board, and the parts flagged on it. */
html.portal .order-placement-stage { position: relative; height: min(62vh, 36rem); }
html.portal .order-placement-stage .fab-stage, html.portal .order-placement-stage .view3d-stage { position: absolute; inset: 0; min-width: 0; height: auto; }
html.portal .portal-marker circle { pointer-events: all; }
html.portal .portal-marker.is-flagged circle { stroke: var(--coast-sun); stroke-width: 2; fill: rgba(246, 183, 60, 0.3); }
html.portal .portal-marker.is-flagged line { stroke: var(--coast-sun); }

/* ── Home, for somebody new: how ordering goes, and the one button that starts it ── */
html.portal .portal-welcome { max-width: 52rem; }
html.portal .portal-welcome-gull { width: 4.5rem; height: auto; }
html.portal .portal-howto { counter-reset: howto; list-style: none; padding: 0; margin: 0 0 1.25rem; display: grid; gap: 0.6rem; }
html.portal .portal-howto li { counter-increment: howto; position: relative; padding-left: 2.4rem; }
html.portal .portal-howto li::before {
    content: counter(howto); position: absolute; left: 0; top: 0.05rem;
    width: 1.7rem; height: 1.7rem; border-radius: 50%;
    display: grid; place-items: center; font-weight: 600; font-size: 0.85rem;
    background: var(--bs-primary); color: #fff;
}
/* The navbar's one button. Sits in a list of links, so it needs its own vertical room when collapsed. */
html.portal .portal-new-order { white-space: nowrap; }
@media (max-width: 991.98px) { html.portal .portal-new-order { margin: 0.35rem 0; } }

/* A list table on a phone: wide enough to read, and scrolled sideways by the wrapper around it,
   rather than squeezed until a board's name breaks a letter to a line (home, Boards). */
.portal-table { min-width: 40rem; }
.portal-table .board-thumb-row { min-width: 13rem; }

/* Maintenance mode (Manage > Portal): what would change something is grayed out and does not
   take a click. The html element carries .portal-maintenance while it is on (portal/base.html);
   portal.js disables the same controls for the keyboard. Selectors rather than a flag in every
   template, so a card fetched into the page later is covered too. core.maintenance refuses the
   request anyway if one gets through. */
html.portal-maintenance form[method="post" i]:not([data-maintenance-ok]) :is(button:not([type="button"]), input[type="submit"], input[type="file"]),
html.portal-maintenance [data-card-use],
html.portal-maintenance [data-maintenance-write] {
    pointer-events: none;
    opacity: 0.5;
}
html.portal-maintenance .portal-maintenance-hide { display: none !important; }
.portal-maintenance-banner { text-align: center; }

/* ─── An order's progress ────────────────────────────────────────────────── */
/* portal/partials/order_progress.html. The full bar (the order page) is a row of numbered dots,
   checkout to delivery, on a track filled to the current one; on a phone the dots run down the
   page instead, each with its words and date beside it, under the same track drawn as a plain
   bar. The compact bar (the lists) is the track and a line of words. Colors are the theme's
   tokens, so the night beach needs nothing of its own: done is Bootstrap's green, where it is
   now the sea teal, still to come the sand. */
html.portal .order-progress { --dot: 1.75rem; position: relative; }
html.portal .order-progress-track {
    height: 4px; border-radius: 2px; overflow: hidden;
    background: var(--bs-border-color);
}
html.portal .order-progress-fill { height: 100%; background: var(--bs-success); }
html.portal .order-progress.is-withdrawn .order-progress-fill { background: var(--bs-secondary-color); }
html.portal .order-progress:not(.is-compact) .order-progress-track {
    /* From the first dot's center to the last's: each step is 1/steps of the row. */
    position: absolute; top: calc(var(--dot) / 2 - 2px);
    left: calc(100% / (var(--steps) * 2)); right: calc(100% / (var(--steps) * 2));
}
html.portal .order-progress-steps {
    position: relative; display: flex; list-style: none; margin: 0; padding: 0;
}
html.portal .order-progress-step {
    flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center;
    text-align: center; font-size: 0.8rem; padding: 0 0.15rem;
}
html.portal .order-progress-dot {
    width: var(--dot); height: var(--dot); flex: none; border-radius: 50%;
    display: grid; place-items: center; font-size: 0.8rem; font-weight: 600;
    border: 2px solid var(--bs-border-color); background: var(--bs-body-bg); color: var(--bs-secondary-color);
}
html.portal .order-progress-step.is-done .order-progress-dot {
    background: var(--bs-success); border-color: var(--bs-success); color: #fff;
}
html.portal .order-progress-step.is-current .order-progress-dot {
    background: var(--bs-primary); border-color: var(--bs-primary); color: var(--accent-contrast);
    box-shadow: 0 0 0 0.25rem var(--bs-focus-ring-color);
}
/* Waiting on the customer's answer: their step to act on. */
html.portal .order-progress.is-asked .order-progress-step.is-current .order-progress-dot {
    background: var(--coast-sun); border-color: var(--coast-sun); color: #1b1b1b;
}
html.portal .order-progress-step.is-withdrawn .order-progress-dot {
    background: var(--bs-secondary-color); border-color: var(--bs-secondary-color); color: var(--bs-body-bg);
}
html.portal .order-progress-words { display: flex; flex-direction: column; margin-top: 0.4rem; line-height: 1.2; }
html.portal .order-progress-step.is-todo .order-progress-label { color: var(--bs-secondary-color); }
html.portal .order-progress-step:is(.is-current, .is-withdrawn) .order-progress-label { font-weight: 600; color: var(--bs-emphasis-color); }
html.portal .order-progress-date { font-size: 0.75rem; color: var(--bs-secondary-color); }
/* Expected, not reached: shown by the arrow, never by color alone. */
html.portal .order-progress-date.is-eta::before { content: '\2192\00a0'; }
html.portal .order-progress-step.is-current .order-progress-date.is-eta { color: var(--bs-emphasis-color); }
html.portal .order-promise { font-size: 0.8rem; font-weight: 600; white-space: normal; text-align: left; }
html.portal .order-promise.text-bg-light { color: var(--bs-body-color) !important; }
html.portal .order-tracking-carrier { white-space: nowrap; }
/* A withdrawn order is two steps; spread across the card they look lost. */
html.portal .order-progress.is-withdrawn:not(.is-compact) { max-width: 22rem; }

@media (max-width: 575.98px) {
    html.portal .order-progress:not(.is-compact) .order-progress-track {
        position: static; margin-bottom: 0.9rem;
    }
    html.portal .order-progress-steps { flex-direction: column; }
    html.portal .order-progress-step {
        position: relative; flex-direction: row; align-items: flex-start; gap: 0.6rem;
        text-align: left; padding: 0 0 0.7rem; font-size: 0.875rem;
    }
    /* The line from each dot down to the next: green once that step is done. */
    html.portal .order-progress-step:not(:last-child)::before {
        content: ''; position: absolute; left: calc(var(--dot) / 2 - 1px); top: var(--dot); bottom: 0;
        width: 2px; background: var(--bs-border-color);
    }
    html.portal .order-progress-step.is-done:not(:last-child)::before { background: var(--bs-success); }
    html.portal .order-progress-words {
        flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 0 0.5rem;
        margin-top: 0; padding-top: 0.2rem;
    }
}

html.portal .order-progress.is-compact { max-width: 24rem; }
html.portal .order-progress.is-compact .order-progress-track { height: 6px; border-radius: 3px; margin-top: 0.2rem; }
html.portal .order-progress.is-compact .order-progress-text { color: var(--bs-secondary-color); }
html.portal .order-progress-cell .order-progress { margin-top: 0.25rem; min-width: 12rem; }


/* ─── Home: one next step, and who has the ball ───────────────────────────── */
/* portal/org_home.html: the top card says whose move it is (sun: the customer's, sea: ours); the rows (partials/home_row.html) are wrapping flex lines, so a phone stacks them. */
html.portal .home-next { border-width: 2px; }
html.portal .home-next.is-you { border-color: var(--coast-sun); }
html.portal .home-next.is-us { border-color: var(--bs-primary-border-subtle); }
html.portal .home-ball {
    font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; font-size: 0.8rem;
    color: var(--bs-emphasis-color); margin-bottom: 0.35rem;
}
html.portal .home-next.is-you .home-ball .fa-circle { color: var(--coast-sun); }
html.portal .home-next .home-row { padding: 0; border: 0; }
html.portal .home-next .home-row-action { font-size: 1rem; padding: 0.5rem 1.1rem; }
html.portal .home-rows { margin: 0; }
html.portal .home-row {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem;
    padding: 0.6rem 0.9rem; border-top: 1px solid var(--bs-border-color);
}
html.portal .home-row:first-child { border-top: 0; }
html.portal .home-row-what { flex: 1 1 16rem; min-width: 0; }
html.portal .home-row-what > div { min-width: 0; overflow-wrap: anywhere; }
html.portal .home-row-where { flex: 1 1 14rem; min-width: 0; }
html.portal .home-row-action { flex: none; margin-left: auto; white-space: nowrap; }
/* Our usual wait, beside the wait. */
html.portal .wait-chip {
    display: inline-flex; align-items: center; gap: 0.3rem; white-space: nowrap;
    font-size: 0.8rem; padding: 0.1rem 0.55rem; border-radius: 999px;
    background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis);
    border: 1px solid var(--bs-primary-border-subtle);
}
/* Short name, exact form in its tooltip: "v2" for revision 2026101001. */
html.portal .rev-name { cursor: help; text-decoration: underline dotted; text-underline-offset: 0.15em; }

/* ─── Getting to the page, and hitting what is on it ──────────────────────── */
/* First tab stop: skips the navbar. */
html.portal .skip-link {
    position: absolute; left: 0.5rem; top: -4rem; z-index: 2000;
    padding: 0.6rem 1rem; border-radius: 0.375rem;
    background: var(--bs-primary); color: var(--accent-contrast); font-weight: 600;
}
html.portal .skip-link:focus { top: 0.5rem; }

/* Touch targets are at least 44 px (WCAG 2.5.5). `py-0`/`p-0` are !important Bootstrap utilities, so the height comes from min-height with the content centered. */
@media (pointer: coarse), (max-width: 575.98px) {
    html.portal :is(.btn, .nav-link, .dropdown-item, .page-link, .form-select, .form-control:not(textarea)) { min-height: 44px; }
    html.portal .btn { min-width: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; }
    html.portal .btn-close { min-width: 44px; min-height: 44px; box-sizing: border-box; }
    html.portal .btn-group > .btn { display: inline-flex; }
    html.portal .nav-link { display: flex; align-items: center; }
    /* Not a flex box: a summary that is not a list item loses its marker. */
    html.portal body summary { min-height: 44px; box-sizing: border-box; padding-block: 0.65rem; }
    html.portal body :is(.order-swatch, .guide-swatch-pick) { min-width: 44px; min-height: 44px; }
    html.portal .navbar-toggler,
    html.portal main :is(button:not(.btn-close), [role="button"]) { min-height: 44px; }
    /* A transparent border widens the tap area; the box is drawn inside it. */
    html.portal .form-check-input {
        width: 44px; height: 44px; margin: 0; float: none; flex: none;
        border: 10px solid transparent; background-clip: padding-box;
        box-shadow: inset 0 0 0 1px var(--bs-secondary-color);
    }
    html.portal .form-check-input[type="radio"], html.portal .form-switch .form-check-input { border-radius: 50%; }
    html.portal :is(td, li, dd, .small, .card-body) > a:not(.btn):not(.badge),
    html.portal a.badge { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; }
    html.portal .form-check { min-height: 44px; display: flex; align-items: center; gap: 0.25rem; padding-left: 0; }
    html.portal .home-row-action { flex: 1 1 100%; }
}

/* ─── A table on a phone is a stack of cards ──────────────────────────────── */
/* `table.portal-stack`: under 576 px the header row is for screen readers only, each row is a card and each cell a line prefixed by its `data-label`. A cell with no label spans the card. Nothing scrolls sideways. */
@media (max-width: 575.98px) {
    html.portal .portal-stack { min-width: 0; display: block; }
    html.portal .portal-stack thead {
        position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
    }
    html.portal .portal-stack :is(tbody, tfoot) { display: block; }
    html.portal .portal-stack tr {
        display: block; padding: 0.5rem 0.75rem; border-top: 1px solid var(--bs-border-color);
    }
    html.portal .portal-stack tbody:first-of-type > tr:first-child { border-top: 0; }
    html.portal .portal-stack :is(td, th) {
        display: flow-root; padding: 0.15rem 0; border: 0; text-align: right;
        white-space: normal !important; overflow-wrap: anywhere; min-width: 0;
    }
    html.portal .portal-stack :is(td, th)[data-label]::before {
        content: attr(data-label); float: left; margin-right: 0.75rem;
        color: var(--bs-secondary-color); font-size: 0.8rem; font-weight: 400;
    }
    html.portal .portal-stack :is(td, th):not([data-label]) { display: block; text-align: left; }
    html.portal .portal-stack td:empty { display: none; }
    html.portal .portal-stack .board-thumb-row { min-width: 0; }
    html.portal .portal-stack td.stack-actions { display: flex; justify-content: flex-end; gap: 0.5rem; }
    html.portal .portal-main { padding-left: 0.75rem; padding-right: 0.75rem; }
}

html.portal .guide-fold > summary { cursor: pointer; }

/* ─── What axe asked for ──────────────────────────────────────────────────── */
/* Links in a sentence need more than color (WCAG 1.4.1): underlined unless a button, badge or list row. */
html.portal main a:not(.btn):not(.badge):not(.list-group-item):not(.nav-link):not(.dropdown-item) {
    text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.18em;
}
/* Half opacity failed contrast (2.3:1): muted and dashed instead. */
html.portal .guide-step.is-locked > span { opacity: 1; color: var(--bs-secondary-color); }
/* A fold is at least 24 px on any screen (WCAG 2.5.8). */
/* :where(), so the touch sizes above still win. */
html.portal :where(summary) { min-height: 24px; }
html.portal :where(main .btn) { min-height: 1.6rem; }
html.portal .guide-kind > summary { padding-block: 0.2rem; }
/* Stacked blocker links need room to hit the right one. */
html.portal .order-review-board div.text-warning-emphasis { padding-block: 0.25rem; }

/* ─── The public quote (/quote/, portal/templates/portal/quote.html) ──────────────────────
   Two columns on a computer; on a phone the price sticks to the bottom, so a choice and its
   cost are seen together. */
html.portal .quote { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; gap: 1.25rem; max-width: 76rem; margin: 0 auto; align-items: start; }
html.portal .quote-main { display: grid; gap: 0.9rem; min-width: 0; }
html.portal .quote-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1rem; }
html.portal .quote-private { font-size: 0.8125rem; color: var(--bs-success-text-emphasis); background: var(--bs-success-bg-subtle); border-radius: 999px; padding: 0.15rem 0.7rem; cursor: help; }
html.portal .quote-drop { display: flex; align-items: center; gap: 1rem; padding: 1.4rem 1.25rem; border: 2px dashed var(--bs-border-color); border-radius: 0.75rem; background: var(--bs-tertiary-bg); cursor: pointer; transition: border-color 0.15s, background 0.15s; }
html.portal .quote-drop:hover, html.portal .quote-drop.is-over, html.portal .quote-drop:focus-within { border-color: var(--accent); background: var(--bs-primary-bg-subtle); }
html.portal .quote-drop-icon { font-size: 2rem; color: var(--accent); line-height: 1; }
html.portal .quote-drop-words { display: grid; flex: 1 1 auto; min-width: 0; }
html.portal .quote.is-reading .quote-drop { opacity: 0.6; pointer-events: none; }
html.portal .quote-status:empty { display: none; }
html.portal .quote-status.is-problem { color: var(--bs-warning-text-emphasis); }
html.portal .quote-preview { position: relative; display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; min-height: 6rem; }
html.portal .quote-face { margin: 0; flex: 1 1 14rem; max-width: 26rem; text-align: center; }
html.portal .quote-face canvas { max-width: 100%; max-height: 16rem; width: auto; height: auto; }
html.portal .quote-facts { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center; margin-top: 0.6rem; }
html.portal .quote-facts:empty { display: none; }
html.portal .quote-chip { font-size: 0.8125rem; border: 1px solid var(--bs-border-color); border-radius: 999px; padding: 0.1rem 0.6rem; white-space: nowrap; cursor: help; }
html.portal .quote-numbers { display: grid; grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); gap: 0.6rem 1rem; }
html.portal .quote-numbers label { display: grid; gap: 0.2rem; margin: 0; }
html.portal .quote-parts { margin-top: 0.6rem; }
html.portal .quote-parts[hidden], html.portal .quote-option[hidden] { display: none; }
html.portal .quote-imp-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
html.portal .quote-imp-head .quote-option { flex: 1 1 auto; }
html.portal .quote-imp-more { margin-top: 0.4rem; align-items: end; }
html.portal .quote-imp-more[hidden], html.portal .quote-asm[hidden], html.portal .quote-imp-stackup[hidden] { display: none; }
html.portal .quote-imp-tolerance { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; }
html.portal .quote-imp-tolerance .d-block { flex-basis: 100%; }
html.portal .quote-imp-stackup, html.portal .quote-imp-next { grid-column: 1 / -1; }
html.portal .quote-asm { margin-top: 0.6rem; border: 1px solid var(--bs-border-color); border-radius: 0.5rem; padding: 0.5rem 0.75rem; }
html.portal .quote-asm .quote-facts { justify-content: flex-start; margin-top: 0; margin-bottom: 0.35rem; }
html.portal .quote-option + .quote-option { border-top: 1px dashed var(--bs-border-color); }
html.portal .quote-side { position: sticky; top: 1rem; }
html.portal .quote-expand { display: none; position: absolute; top: 0.6rem; left: 0.25rem; z-index: 1; min-width: 2.75rem; min-height: 2.75rem; align-items: center; justify-content: center; }
html.portal .quote-price-card { position: relative; }
html.portal .quote-total { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.75rem; }
html.portal .quote-total-figure { font-size: 2.25rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
html.portal .quote-total-figure.is-empty { color: var(--bs-secondary-color); }
html.portal .quote-total-notes { display: flex; align-items: center; gap: 0.5rem; }
html.portal .quote-total-notes .badge { cursor: help; }
html.portal .quote-ship { margin-top: 0.35rem; cursor: help; }
html.portal .quote-breaks { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.3rem; margin: 0.7rem 0; }
html.portal .quote-break { display: grid; padding: 0.2rem 0.1rem; border: 1px solid var(--bs-border-color); border-radius: 0.4rem; background: transparent; color: inherit; line-height: 1.2; }
html.portal .quote-break:hover { border-color: var(--accent); }
html.portal .quote-break.is-current { border-color: var(--accent); background: var(--bs-primary-bg-subtle); font-weight: 600; }
html.portal .quote-break-count { font-size: 0.875rem; }
html.portal .quote-break-each { font-size: 0.6875rem; color: var(--bs-secondary-color); font-variant-numeric: tabular-nums; }
html.portal .quote-lines > summary, html.portal .quote-look > summary { cursor: pointer; }
html.portal .quote-look { margin-top: 0.4rem; color: var(--bs-warning-text-emphasis); }
html.portal #quotePrice.is-stale > :not([data-quote-hint]) { opacity: 0.45; }
html.portal .quote-hint { margin-top: 0.5rem; color: var(--bs-warning-text-emphasis); }
@media (max-width: 991.98px) {
    html.portal .quote { grid-template-columns: minmax(0, 1fr); }
    html.portal .quote-side { position: sticky; bottom: 0; top: auto; z-index: 5; margin: 0 -0.75rem; }
    html.portal .quote-price-card { border-radius: 0.75rem 0.75rem 0 0; box-shadow: 0 -0.35rem 1rem rgba(0, 0, 0, 0.18); }
    /* A bar: total, ship date and the way on. The chevron (quote.js) opens the rest over the form. */
    html.portal .quote-price-card .card-body { max-height: 55vh; overflow-y: auto; padding: 0.6rem 0.9rem 0.3rem 2.75rem; }
    html.portal .quote-price-card .card-footer { padding: 0.4rem 0.9rem 0.6rem; }
    html.portal .quote-price-card:not(.is-open) :is(.quote-breaks, .quote-lines, .quote-look, .quote-next) { display: none; }
    html.portal .quote-total-figure { font-size: 1.6rem; }
    html.portal .quote-ship { margin-top: 0; }
    html.portal .quote-expand { display: inline-flex; }
    html.portal .quote-main { padding-bottom: 1rem; }
    html.portal .quote-drop { padding: 1rem; }
}
/* The saved quote, offered after sign-in (partials/quote_resume.html). */
html.portal .quote-resume { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.9rem; padding: 0.5rem 1rem; background: var(--bs-primary-bg-subtle); border-bottom: 1px solid var(--bs-border-color); }
html.portal .quote-resume[hidden] { display: none; }

/* ─── One drop, one review page (portal/setup_review.html) ──────────────────────
   The drop zone is big and dashed with no files, one slim line with them. The whole window
   takes a drop (setup.js). */
html.portal .drop-zone {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.75rem 1rem;
    padding: 1.5rem 1.25rem; border: 2px dashed var(--bs-border-color); border-radius: 0.75rem;
    background: var(--bs-body-bg);
}
html.portal .drop-zone.is-slim { padding: 0.6rem 0.9rem; border-width: 1px; }
html.portal .drop-zone-what { display: flex; align-items: center; gap: 0.75rem; flex: 1 1 16rem; min-width: 0; }
html.portal .drop-zone-what > i { font-size: 1.75rem; color: var(--accent); }
html.portal .drop-zone.is-slim .drop-zone-what > i { font-size: 1.1rem; }
html.portal .drop-zone-choose { min-height: 2.75rem; display: inline-flex; align-items: center; gap: 0.4rem; }
html.portal .drop-zone-progress { flex: 1 1 100%; display: grid; gap: 0.25rem; }
html.portal .drop-zone-progress .progress { height: 0.5rem; }
html.portal .drop-zone.is-busy .drop-zone-choose { pointer-events: none; opacity: 0.6; }
html.portal .drop-zone [data-drop-error] { flex: 1 1 100%; }
html.portal.is-dropping .drop-zone { border-color: var(--accent); background: var(--bs-primary-bg-subtle); }
html.portal.is-dropping body::after {
    content: "Drop to upload"; position: fixed; inset: 0; z-index: 1080; display: grid; place-items: center;
    font-size: 1.5rem; font-weight: 600; color: var(--accent-contrast, #fff);
    background: color-mix(in srgb, var(--accent) 82%, transparent); pointer-events: none;
}

/* Setup rows: a tick and one line when read cleanly, open when it asks something. */
html.portal .review-rows { display: grid; gap: 0.5rem; }
html.portal .review-row { border: 1px solid var(--bs-border-color); border-radius: 0.6rem; background: var(--bs-body-bg); }
html.portal .review-row.is-ask { border-color: var(--bs-warning-border-subtle); box-shadow: inset 0.25rem 0 0 var(--bs-warning); }
html.portal .review-row.is-wait { border-style: dashed; }   /* not faded: faded text fails contrast */
html.portal .review-row-head {
    display: flex; align-items: center; flex-wrap: wrap; gap: 0.35rem 0.75rem; padding: 0.6rem 0.9rem;
    min-height: 2.75rem; list-style: none; cursor: default;
}
html.portal summary.review-row-head { cursor: pointer; }
/* A head that does not fold keeps the chevron's room, so the marks line up. */
html.portal div.review-row-head::before { content: ""; flex: none; width: 1em; margin-right: 0.25rem; }
html.portal .review-mark {
    flex: none; width: 1.5rem; height: 1.5rem; border-radius: 50%; display: grid; place-items: center;
    font-size: 0.8rem; font-weight: 700; background: var(--bs-secondary-bg); color: var(--bs-secondary-color);
}
html.portal .review-row.is-done .review-mark { background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); }
html.portal .review-row.is-ask .review-mark { background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); }
html.portal .review-row-title { font-weight: 600; }
html.portal .review-row-title > i { color: var(--bs-secondary-color); width: 1.1rem; text-align: center; }
html.portal .review-row-summary { color: var(--bs-secondary-color); flex: 1 1 12rem; min-width: 0; }
html.portal .review-auto {
    font-size: 0.6875rem; padding: 0.05rem 0.4rem; border-radius: 1rem; cursor: help;
    border: 1px solid var(--bs-border-color); color: var(--bs-secondary-color);
}
html.portal .review-row-open { min-width: 2.75rem; min-height: 2.75rem; display: inline-grid; place-items: center; }
html.portal .review-row-body { padding: 0 0.9rem 0.75rem 5.15rem; display: grid; gap: 0.4rem; }
html.portal .review-problem { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 0.75rem; }
html.portal .review-problem .btn { min-height: 2.75rem; }
html.portal .review-detail { margin: 0; padding-left: 1.1rem; columns: 2 16rem; color: var(--bs-secondary-color); }
html.portal .review-scope .btn { min-height: 2.75rem; }
@media (max-width: 575.98px) { html.portal .review-row-body { padding-left: 0.9rem; } }

/* ─── The setup's price, pinned (partials/setup_price_card.html) ─────────────────
   A bar along the window bottom on every setup page; opens to show what the price is made of.
   The page keeps room for it underneath. */
html.portal body:has(.setup-price) .portal-main { padding-bottom: 7.5rem; }
html.portal .setup-price {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 1030;
    background: var(--bs-body-bg); border-top: 1px solid var(--bs-border-color);
    box-shadow: 0 -0.35rem 1rem rgba(0, 0, 0, 0.14);
}
html.portal .setup-price-inner { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 1rem; max-width: 100rem; margin: 0 auto; }
html.portal .setup-price-body { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 0.35rem 1.25rem; }
html.portal .setup-price.is-loading .setup-price-body { opacity: 0.55; }
html.portal .setup-price-main { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.15rem 0.9rem; }
html.portal .setup-price-figure { font-size: 1.6rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; cursor: help; }
html.portal .setup-price-figure.is-empty { color: var(--bs-secondary-color); cursor: default; }
html.portal .setup-price-sub { display: flex; gap: 0.5rem; font-size: 0.875rem; }
html.portal .setup-price-ship { cursor: help; white-space: nowrap; }
html.portal .setup-price-why { display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem 0.5rem; }
html.portal .setup-price-qty { display: flex; align-items: stretch; gap: 0.3rem; flex-wrap: wrap; }
html.portal .setup-price-break {
    display: grid; min-width: 3.1rem; min-height: 2.75rem; padding: 0.15rem 0.35rem; line-height: 1.15;
    border: 1px solid var(--bs-border-color); border-radius: 0.4rem; background: transparent; color: inherit;
}
html.portal .setup-price-break:hover { border-color: var(--accent); }
html.portal .setup-price-break.is-current { border-color: var(--accent); background: var(--bs-primary-bg-subtle); font-weight: 600; }
html.portal .setup-price-break-count { font-size: 0.875rem; }
html.portal .setup-price-break-each { font-size: 0.6875rem; color: var(--bs-secondary-color); font-variant-numeric: tabular-nums; }
html.portal .setup-price-count input { width: 5rem; min-height: 2.75rem; }
html.portal .setup-price-more { display: none; flex: 1 1 100%; max-height: 45vh; overflow-y: auto; padding-top: 0.4rem; border-top: 1px dashed var(--bs-border-color); }
html.portal .setup-price.is-open .setup-price-more { display: block; }
html.portal .setup-price-speed { display: flex; gap: 0.3rem; margin-bottom: 0.4rem; }
html.portal .setup-price-speed .btn { min-height: 2.75rem; }
html.portal .setup-price-actions { flex: none; display: flex; align-items: center; gap: 0.4rem; }
html.portal .setup-price-expand { flex: none; min-width: 2.75rem; min-height: 2.75rem; }
html.portal .setup-price .setup-finish, html.portal .setup-price-actions > a.btn, html.portal .setup-price-cart { min-height: 2.75rem; display: inline-flex; align-items: center; gap: 0.4rem; white-space: nowrap; }
@media (max-width: 767.98px) {
    html.portal body:has(.setup-price) .portal-main { padding-bottom: 9.5rem; }
    html.portal .setup-price-inner { padding: 0.4rem 0.75rem; align-items: flex-end; }
    html.portal .setup-price-figure { font-size: 1.35rem; }
    /* Folded on a phone: total, date and button; quantities open with the rest. */
    html.portal .setup-price:not(.is-open) .setup-price-qty { display: none; }
    html.portal .setup-price-actions { flex-direction: column-reverse; align-items: flex-end; gap: 0.2rem; }
}
html.portal .review-row.is-ready .review-mark { background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); }

/* ─── Order again: the sheet, Home's cards ────────────────────────────────── */
/* partials/reorder_sheet.html in #reorderModal (reorder.js). On a phone the dialog is a bottom sheet, the button within a thumb's reach. */
html.portal .reorder-lines { margin: 0.5rem 0 0.75rem; }
html.portal .reorder-line { align-items: center; gap: 0.75rem; padding: 0.5rem 0; border-top: 1px solid var(--bs-border-color); }
html.portal .reorder-line:first-child { border-top: 0; }
html.portal .reorder-line-what { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
html.portal .reorder-qty { flex: none; width: 9.5rem; flex-wrap: nowrap; }
html.portal .reorder-qty .form-control { font-weight: 600; padding-inline: 0.25rem; -moz-appearance: textfield; appearance: textfield; }
html.portal .reorder-qty .form-control::-webkit-outer-spin-button,
html.portal .reorder-qty .form-control::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
html.portal .reorder-figures { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 0.25rem 1rem; }
html.portal .reorder-price { font-size: 2rem; font-weight: 700; line-height: 1.1; color: var(--bs-emphasis-color); }
html.portal .reorder-currency { font-size: 0.85rem; font-weight: 400; color: var(--bs-secondary-color); margin-left: 0.35rem; }
html.portal .reorder-date { color: var(--bs-emphasis-color); white-space: nowrap; }
html.portal .reorder-date .fa-solid { color: var(--accent); margin-right: 0.25rem; }
html.portal .reorder-facts { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.6rem 0 0.9rem; }
html.portal .reorder-chip {
    display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%;
    padding: 0.15rem 0.6rem; border-radius: 999px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    background: var(--bs-tertiary-bg); color: var(--bs-emphasis-color); border: 1px solid var(--bs-border-color);
}
html.portal .reorder-chip.is-auto { background: var(--bs-success-bg-subtle); border-color: var(--bs-success-border-subtle); color: var(--bs-success-text-emphasis); }
html.portal .reorder-blockers { list-style: none; padding: 0; margin: 0.6rem 0 0.9rem; color: var(--bs-danger-text-emphasis); }
html.portal .reorder-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
html.portal .reorder-actions .reorder-place { flex: 1 1 12rem; }
html.portal [data-reorder-live].is-stale .reorder-figures { opacity: 0.5; }
html.portal .reorder-page { max-width: 34rem; }
@media (max-width: 575.98px) {
    html.portal .reorder-modal .modal-dialog { margin: 0; min-height: 100%; align-items: flex-end; }
    html.portal .reorder-modal .modal-content { border-radius: 1rem 1rem 0 0; border-bottom: 0; }
    html.portal .reorder-actions .btn { flex: 1 1 100%; }
}

html.portal .again-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); gap: 0.75rem; }
html.portal .again-card .card-body { align-items: center; padding: 0.6rem 0.75rem; }
html.portal .again-card-what { flex: 1 1 auto; min-width: 0; }
html.portal .reorder-button { white-space: nowrap; }


/* ─── The order history ───────────────────────────────────────────────────── */
/* portal/orders.html: three totals, search and two filters on one line (stacked on a phone), then the orders. */
html.portal .history-totals { display: flex; flex-wrap: wrap; gap: 0.5rem; }
html.portal .history-total {
    display: flex; align-items: baseline; gap: 0.4rem; padding: 0.4rem 0.9rem; border-radius: 0.5rem;
    background: var(--bs-body-bg); border: 1px solid var(--bs-border-color);
}
html.portal .history-total-n { font-size: 1.35rem; font-weight: 700; color: var(--bs-emphasis-color); }
html.portal .history-total-l { color: var(--bs-secondary-color); font-size: 0.85rem; }
html.portal .history-filter { display: flex; flex-wrap: wrap; gap: 0.5rem; }
html.portal .history-filter .history-search { flex: 2 1 14rem; width: auto; }
html.portal .history-filter .form-select { flex: 1 1 10rem; width: auto; }
html.portal .history-board { align-items: center; padding: 0.15rem 0; }
html.portal .history-repeat { text-decoration: none; }
html.portal .history-table td { padding-block: 0.5rem; }
@media (max-width: 575.98px) {
    html.portal .history-total { flex: 1 1 28%; flex-direction: column; gap: 0; padding: 0.4rem 0.6rem; }
    html.portal .history-table .history-board { justify-content: flex-end; text-align: right; }
}


/* ─── Account: your defaults, and the address book ────────────────────────── */
/* partials/org_defaults.html: a tile per default, answers drawn as on the order form, "Any" first. Addresses are a grid of cards. */
html.portal .defaults-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 0.75rem 1.25rem; }
html.portal .defaults-tile { min-width: 0; padding-bottom: 0.5rem; }
html.portal .defaults-tile-head { font-size: 0.8rem; font-weight: 600; color: var(--bs-secondary-color); margin-bottom: 0.35rem; }
html.portal .defaults-tile-head .fa-solid { width: 1.1rem; text-align: center; color: var(--accent); }
html.portal .defaults-choices { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
html.portal .defaults-choices .btn-check:checked + .btn { background: var(--bs-primary-bg-subtle); color: var(--bs-emphasis-color); border-color: var(--accent); font-weight: 600; }
html.portal .defaults-choices .defaults-any { border-style: dashed; }
html.portal .defaults-people .form-check { margin-bottom: 0.1rem; }
html.portal fieldset:disabled .defaults-choices { opacity: 0.75; }
html.portal .address-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: 0.75rem; align-items: start; }
html.portal .address-card.is-default { border-color: var(--accent); }
html.portal .address-card.is-new { border-style: dashed; }
html.portal .address-lines { white-space: pre-line; overflow-wrap: anywhere; }
html.portal .address-form { display: grid; gap: 0.4rem; margin-top: 0.4rem; }
html.portal .address-form-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; }
/* Saved addresses above the cart's "Ship to": one tap fills the box. */
html.portal .address-picks { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.35rem; }

/* The review page's parts-list lines and design-check findings. */
html.portal .review-lines { display: grid; gap: 0.1rem; }
html.portal .review-lines .bom-issue { margin-bottom: 0.3rem; }
html.portal .review-problem .badge { vertical-align: 0.1em; }
html.portal .review-row-body > .btn { justify-self: start; }
/* Stand-alone links are touch targets: 44 px, as everywhere on the portal. */
html.portal .dfm-line-link { display: inline-flex; align-items: center; min-height: 2.75rem; }
html.portal .portal-touch { min-height: 2.75rem; display: inline-flex; align-items: center; gap: 0.35rem; }
