/* ==========================================================================
   Brite Lotus — Components
   Buttons, form fields, floating menubar + mega menu, accordion,
   the panel/footer reveal, and entrance animation utilities.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

/* THE BUTTON SYSTEM
   ---------------------------------------------------------------------------
   One set of state rules, read from custom properties. A variant declares
   VALUES; it does not declare a model. That is the whole point: before this,
   --primary hollowed out, --dark and --blue darkened, and --ghost did nothing
   at all, which meant four buttons sitting next to each other behaved four
   different ways and a new one inherited whichever was copied first.

   The properties a variant may set:

     --btn-fill / --btn-ink / --btn-edge          rest
     --btn-fill-hover / --btn-ink-hover /
       --btn-edge-hover / --btn-glow              hover and keyboard focus
     --btn-fill-press                             the pressed step

   Anything left unset falls back sensibly, so a variant is usually two or
   three lines.

   THE GROUND RULE. A button's hover always moves AWAY from the ground it sits
   on, so the change is legible wherever it lands:

     on a light ground   the fill deepens
     on a dark ground    the fill hollows out to an outline and lights up

   That is not two models fighting; it is one rule — increase separation from
   the ground — with the only implementation that works on each. A yellow
   button hollowed out on white would disappear, and a yellow button deepened
   on black barely changes. Grounds are declared by .on-light (the page panel)
   and .bl-on-dark / .bl-ground-night (the hero, the footer, a dark band
   nested inside a light page).

   KEYBOARD PARITY. :focus-visible is deliberately grouped with :hover
   everywhere below. Someone tabbing gets the same affordance a mouse user
   gets, plus the focus ring on top. Splitting them — an outline for the
   keyboard and a real visual change for the mouse — is the commonest way a
   keyboard user ends up unsure what is selected.
   --------------------------------------------------------------------------- */

.bl-btn {
  /* Rest */
  --btn-fill: transparent;
  --btn-ink: currentColor;
  --btn-edge: var(--btn-fill);

  /* Hover and keyboard focus. Default to rest, so a variant that sets
     nothing simply does not react rather than reacting wrongly. */
  --btn-fill-hover: var(--btn-fill);
  --btn-ink-hover: var(--btn-ink);
  --btn-edge-hover: var(--btn-fill-hover);
  --btn-glow: none;

  /* Press */
  --btn-fill-press: var(--btn-fill-hover);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--bl-space-3);
  padding: 0.875rem 1.75rem;
  border: 2px solid var(--btn-edge);
  border-radius: var(--bl-radius-pill);
  background: var(--btn-fill);
  color: var(--btn-ink);
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.01em;
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--bl-dur-base) var(--bl-ease-out),
    color var(--bl-dur-base) var(--bl-ease-out),
    border-color var(--bl-dur-base) var(--bl-ease-out),
    box-shadow var(--bl-dur-base) var(--bl-ease-out),
    transform var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-btn:hover,
.bl-btn:focus-visible {
  background: var(--btn-fill-hover);
  border-color: var(--btn-edge-hover);
  color: var(--btn-ink-hover);
  box-shadow: var(--btn-glow);
}

/* The press. One rule for every button: it moves a pixel down and the fill
   takes one more step. Before this only four elements on the whole site
   acknowledged being clicked. */
.bl-btn:active {
  transform: translateY(1px);
  background: var(--btn-fill-press);
}

/* Disabled and busy.

   [disabled] alone missed two real cases: an <a> styled as a button, which
   cannot carry the attribute, and a control inside a disabled fieldset. The
   :disabled pseudo covers the fieldset case and aria-disabled covers the
   link, so all three now look the same. pointer-events: none would swallow
   the cursor feedback, so not-allowed is set before it on the wrapper. */
.bl-btn:disabled,
.bl-btn[disabled],
.bl-btn[aria-disabled="true"],
.bl-btn[aria-busy="true"] {
  opacity: 0.55;
  pointer-events: none;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}

/* Busy is not disabled.

   It shares the rule above because both are untouchable, but it is a
   different message: a disabled control will not do anything, a busy one is
   doing it right now. So it dims less — enough to read as inert, not so much
   that the spinner inside it goes faint. */
.bl-btn[aria-busy="true"] { opacity: 0.8; }

/* The spinner.

   A flex child rather than a positioned overlay, so it takes the button's own
   gap and sits beside the label without anything having to know how wide the
   label is. Sized in em, so it tracks the button's type at every size.

   It appears the moment aria-busy does, and it is the same attribute assistive
   tech reads — one state, announced and drawn from one place. */
.bl-btn[aria-busy="true"]::after {
  content: "";
  flex: 0 0 auto;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: bl-btn-spin 0.7s linear infinite;
}

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

/* Reduced motion does not mean no feedback — a frozen ring reads as broken,
   which is worse than the spin it replaced. A ring that breathes says the
   same thing without anything travelling across the screen. */
@media (prefers-reduced-motion: reduce) {
  .bl-btn[aria-busy="true"]::after {
    border-top-color: currentColor;
    animation: bl-btn-breathe 1.4s ease-in-out infinite;
  }
}

@keyframes bl-btn-breathe { 50% { opacity: 0.25; } }

/* ---- Primary: the conversion button. Yellow is reserved for "sign up". --- */

.bl-btn--primary {
  --btn-fill: var(--bl-yellow);
  --btn-ink: var(--bl-black);
  /* Default is the DARK-ground behavior, because the hero and the footer are
     where this button mostly lives. .on-light overrides it below. */
  --btn-fill-hover: transparent;
  --btn-ink-hover: var(--bl-yellow);
  --btn-edge-hover: var(--bl-yellow);
  --btn-glow: var(--bl-glow-yellow);
  --btn-fill-press: rgba(230, 255, 0, 0.16);
}

/* On the light page panel it deepens instead — see THE GROUND RULE above. */
.on-light .bl-btn--primary {
  --btn-fill-hover: var(--bl-yellow-dim);
  --btn-ink-hover: var(--bl-black);
  --btn-edge-hover: var(--bl-yellow-dim);
  --btn-glow: none;
  --btn-fill-press: #b9cc00;
}

/* A dark band nested inside a light page is still a dark ground, so it takes
   the hollow-out back. Last, so it wins over .on-light. */
.bl-on-dark .bl-btn--primary,
.bl-ground-night .bl-btn--primary {
  --btn-fill-hover: transparent;
  --btn-ink-hover: var(--bl-yellow);
  --btn-edge-hover: var(--bl-yellow);
  --btn-glow: var(--bl-glow-yellow);
  --btn-fill-press: rgba(230, 255, 0, 0.16);
}

/* ---- Blue: useful, but not the conversion. ------------------------------- */

.bl-btn--blue {
  --btn-fill: var(--bl-blue);
  --btn-ink: var(--bl-white);
  --btn-fill-hover: var(--bl-blue-deep);
  --btn-ink-hover: var(--bl-white);
  --btn-fill-press: #00628c;
}

/* --bl-blue-deep is the DARKER blue in light mode and the LIGHTER one in dark
   mode, which is correct by the ground rule but happens by accident of the
   token. Stated here so it is not "fixed" later by someone reading the token
   in isolation. The pressed step has to be restated for the same reason:
   #00628c is a deepening, and on a dark panel it would be a reversal. */
@media (prefers-color-scheme: dark) {
  body:not(.bl-theme-fixed) .bl-btn--blue {
    --btn-fill-press: #6ecbf3;
  }
}

/* ---- Dark: a solid pill for a light ground. ------------------------------ */

.bl-btn--dark {
  --btn-fill: var(--bl-charcoal);
  --btn-ink: var(--bl-white);
  --btn-fill-hover: var(--bl-black);
  --btn-ink-hover: var(--bl-white);
  --btn-fill-press: #000000;
}

/* On a dark ground the pill inverts, for the same reason the dark theme
   inverts it: charcoal on black is barely a button, and its old hover went to
   pure black, which erased it completely. The style guide makes that obvious
   at a glance — it is what the page is for. */
.bl-on-dark .bl-btn--dark,
.bl-ground-night .bl-btn--dark {
  --btn-fill: var(--bl-off-white);
  --btn-ink: var(--bl-black);
  --btn-fill-hover: var(--bl-white);
  --btn-ink-hover: var(--bl-black);
  --btn-fill-press: #d8d8d8;
}

/* In dark mode the pill inverts to light-on-dark, so its hover has to invert
   with it — deepening to black on a near-black panel was invisible, which is
   what the old rule did. */
@media (prefers-color-scheme: dark) {
  body:not(.bl-theme-fixed) .bl-btn--dark {
    --btn-fill: var(--bl-text);
    --btn-ink: var(--bl-surface);
    --btn-fill-hover: var(--bl-white);
    --btn-ink-hover: var(--bl-surface);
    --btn-fill-press: #cfd2d6;
  }
}

/* ---- Ghost: an outline that borrows the text color around it. ------------ */

.bl-btn--ghost {
  --btn-fill: transparent;
  --btn-edge: currentColor;
  /* It had no hover at all — the only button on the site that did nothing
     when you pointed at it. It fills with a wash of the surrounding ink,
     which works on either ground because it is derived from the text color
     rather than from a fixed value. */
  --btn-fill-hover: color-mix(in srgb, currentColor 12%, transparent);
  --btn-edge-hover: currentColor;
  --btn-fill-press: color-mix(in srgb, currentColor 20%, transparent);
}

/* ---- Sizes and layout. Geometry only; these never touch state. ----------- */

.bl-btn--block { width: 100%; }

.bl-btn--lg {
  padding: 1.0625rem 2rem;
  font-size: 1.0625rem;
}

.bl-btn--sm {
  padding: 0.625rem 1.25rem;
  font-size: var(--bl-size-sm);
}

/* Trailing circular arrow, as on the footer CTA.

   The disc is currentColor, so it flips with the button. The glyph needs the
   opposite: dark on the yellow disc at rest, and dark again on the yellow
   disc once the button hollows out on hover. Driving it through its own
   custom property keeps the glyph readable in both states instead of going
   yellow-on-yellow. */
.bl-btn__arrow {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

/* The glyph is drawn on a symmetric 24×24 viewBox with the artwork centered
   inside it, so it sits dead center in the disc — and it does not shift on
   hover, which is what threw the centering off before. */
.bl-btn__arrow svg {
  width: 0.8125rem;
  height: 0.8125rem;
  color: var(--bl-arrow-glyph, var(--bl-white));
  transition: color var(--bl-dur-base) var(--bl-ease-out);
}

/* The glyph follows the same property pattern as the button itself: a rest
   value and a hover value, declared per ground, with ONE state rule reading
   them. The old version needed four separate :hover rules whose order
   mattered, and the comment on the last one had to explain why.

   The logic never changes: the disc is the button's own color, so the glyph
   takes whatever the button's FILL is at that moment — black glyph on the
   yellow disc, yellow glyph on the black disc. Get it wrong and the arrow is
   yellow on yellow and simply vanishes. */
/* Both values are declared on the base class so the hover rule below never
   has to fall back to the property it is setting.

   THAT MATTERS, and it is not a style preference. Writing
     --bl-arrow-glyph: var(--bl-arrow-glyph-hover, var(--bl-arrow-glyph))
   makes the property reference itself. CSS calls that a cycle and throws the
   whole declaration away as invalid at computed-value time — it does not
   quietly keep the old value. The glyph then fell through to the `white`
   fallback on the svg, which is how a white arrow ended up on a yellow disc
   the moment anyone hovered. Every value the hover rule reads has to exist
   independently of it. */
.bl-btn {
  --bl-arrow-glyph: var(--bl-white);
  --bl-arrow-glyph-hover: var(--bl-white);
}

.bl-btn--primary {
  --bl-arrow-glyph: var(--bl-yellow);       /* disc is black at rest */
  --bl-arrow-glyph-hover: var(--bl-black);  /* disc turns yellow on hover */
}

/* Light ground: the button stays filled, so the disc never changes and
   neither does the glyph. */
.on-light .bl-btn--primary { --bl-arrow-glyph-hover: var(--bl-yellow); }

/* A dark band nested in a light page hollows out like any dark ground, so
   the disc goes yellow and the glyph must go black to stay legible. */
.bl-on-dark .bl-btn--primary,
.bl-ground-night .bl-btn--primary { --bl-arrow-glyph-hover: var(--bl-black); }

.bl-btn:hover,
.bl-btn:focus-visible { --bl-arrow-glyph: var(--bl-arrow-glyph-hover); }

/* --------------------------------------------------------------------------
   The conversion CTA — "Enlighten Me!"

   Every other button on the site is a navigational nudge. This one is the
   entire reason the homepage exists, so it gets its own behavior: it acts
   like a light source rather than inverting the way the footer pill does.
   Inverting would make the most important control on the page *less*
   prominent on hover, which is backwards.

   IT IS BUILT IN LAYERS FOR A REASON. A CSS animation always overrides a
   transition on the same property, so an idle keyframe glow on box-shadow
   and a hover transition on box-shadow cannot coexist: cancelling the
   animation snaps the shadow to nothing before the transition can start,
   which is exactly the "grow, pause, switch on" stutter.

   So nothing is ever cancelled. The idle breath keeps running on its own
   layer for the whole visit, and a second glow layer simply fades in on top
   of it. Both are plain opacity, both ease, and the handoff is invisible.

     .bl-cta__idle   soft pulse, always animating, never interrupted
     .bl-cta__glow   the hover bloom, opacity 0 -> 1 on a long ease
     .bl-cta__clip   the only element that clips, so it can hold the sheen
                     without clipping the glows (box-shadow is not painted
                     under its own element, so these sit outside the button)
     .bl-cta__label  rides on top
   -------------------------------------------------------------------------- */

.bl-cta {
  position: relative;
  isolation: isolate;
  transform: translateZ(0);
  transition:
    transform 420ms var(--bl-ease-enter),
    filter 520ms var(--bl-ease-enter),
    background-color 300ms var(--bl-ease-enter),
    border-color 300ms var(--bl-ease-enter),
    color 300ms var(--bl-ease-enter);
}

.bl-cta__idle,
.bl-cta__glow,
.bl-cta__clip {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

.bl-cta__idle,
.bl-cta__glow { z-index: 0; }
.bl-cta__clip { z-index: 1; overflow: hidden; }
.bl-cta__label { position: relative; z-index: 2; }

/* Idle: a slow, shallow pulse. Long enough (5s) that it never nags, but
   enough to read as alive on a page with one thing to do. It keeps running
   under the hover bloom rather than being switched off. */
.bl-cta__idle { box-shadow: 0 0 26px rgba(230, 255, 0, 0.30); opacity: 0; }

@media (prefers-reduced-motion: no-preference) {
  .bl-cta.is-entered .bl-cta__idle {
    animation: bl-cta-breathe 5s var(--bl-ease-in-out) infinite;
  }
}

@keyframes bl-cta-breathe {
  0%, 100% { opacity: 0; }
  50%      { opacity: 1; }
}

/* Hover bloom. Two shadows, not one — a tight hot ring plus a wide soft
   halo — because a single shadow reads as a flat outline rather than light.
   It eases in over 760ms so it arrives with the growth instead of after it. */
.bl-cta__glow {
  opacity: 0;
  box-shadow:
    0 0 22px rgba(230, 255, 0, 0.55),
    0 0 60px rgba(230, 255, 0, 0.32),
    0 10px 28px rgba(0, 0, 0, calc(0.38 * var(--bl-shadow-boost)));
  transition: opacity 760ms var(--bl-ease-enter);
}

.bl-btn--primary.bl-cta:hover,
.bl-btn--primary.bl-cta:focus-visible {
  background: var(--bl-yellow);
  border-color: var(--bl-yellow);
  color: var(--bl-black);
  transform: translateY(-2px) scale(1.012);
  filter: brightness(1.05) saturate(1.04);
  box-shadow: none;
}

.bl-btn--primary.bl-cta:hover .bl-cta__glow,
.bl-btn--primary.bl-cta:focus-visible .bl-cta__glow { opacity: 1; }

/* Leaving is quicker than arriving — a slow fade-out feels like lag. */
.bl-cta:not(:hover) .bl-cta__glow { transition-duration: 420ms; }

/* The sheen: one narrow specular band per hover, held back slightly so it
   reads as part of the ramp rather than a separate event. */
.bl-cta__sheen {
  position: absolute;
  inset: -50% -60%;
  opacity: 0;
  background: linear-gradient(
    105deg,
    transparent 40%,
    rgba(255, 255, 255, 0.8) 50%,
    transparent 60%
  );
}

@media (prefers-reduced-motion: no-preference) {
  .bl-btn--primary.bl-cta:hover .bl-cta__sheen,
  .bl-btn--primary.bl-cta:focus-visible .bl-cta__sheen {
    animation: bl-cta-sheen 900ms var(--bl-ease-enter) 140ms;
  }
}

@keyframes bl-cta-sheen {
  0%   { opacity: 0;   transform: translateX(-85%); }
  15%  { opacity: 0.85; }
  80%  { opacity: 0.85; }
  100% { opacity: 0;   transform: translateX(85%); }
}

/* Press: settle down and flare, so the click has a physical payoff. */
.bl-btn--primary.bl-cta:active {
  transform: translateY(0) scale(0.992);
  filter: brightness(1.12);
  transition-duration: 90ms;
}

.bl-btn--primary.bl-cta:active .bl-cta__glow {
  opacity: 1;
  box-shadow:
    0 0 34px rgba(230, 255, 0, 0.72),
    0 0 80px rgba(230, 255, 0, 0.42),
    0 4px 14px rgba(0, 0, 0, calc(0.4 * var(--bl-shadow-boost)));
  transition-duration: 90ms;
}

/* While the signup is in flight the button should look busy, not clickable. */
.bl-cta[aria-busy="true"] .bl-cta__idle { animation: none; opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .bl-cta,
  .bl-btn--primary.bl-cta:hover,
  .bl-btn--primary.bl-cta:focus-visible,
  .bl-btn--primary.bl-cta:active { transform: none; }
  .bl-cta__sheen,
  .bl-cta__idle { display: none; }
  .bl-cta__glow { transition-duration: 1ms; }
}

/* --------------------------------------------------------------------------
   Form fields
   -------------------------------------------------------------------------- */

.bl-field {
  position: relative;
  display: block;
}

.bl-field__icon {
  position: absolute;
  top: 50%;
  left: 1.125rem;
  width: 1.125rem;
  height: 1.125rem;
  color: var(--bl-text-faint);
  pointer-events: none;
  /* Above the input: focusing the field transforms the input, which promotes
     it into the positioned paint layer and would otherwise cover the icon. */
  z-index: 2;
  /* Centering lives in transform, not the translate property, so the focus
     scale below composes with it instead of fighting it. */
  transform: translateY(-50%);
  transform-origin: center;
  transition: color var(--bl-dur-base) var(--bl-ease-out),
              transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-input {
  position: relative;
  z-index: 1;
  width: 100%;
  padding: 1.0625rem 1.25rem 1.0625rem 3rem;
  border: 2px solid transparent;
  border-radius: var(--bl-radius-md);
  background: var(--bl-surface-raised);
  color: var(--bl-text);
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-medium);
  line-height: 1.2;
  box-shadow: var(--bl-shadow-sm);
  transition:
    border-color var(--bl-dur-base) var(--bl-ease-out),
    box-shadow var(--bl-dur-base) var(--bl-ease-out),
    transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-input::placeholder {
  color: var(--bl-text-faint);
  font-weight: var(--bl-weight-medium);
  transition: opacity var(--bl-dur-base) var(--bl-ease-out),
              transform var(--bl-dur-base) var(--bl-ease-out);
}

/* Focusing a field lifts it a hair off the page and lights a blue ring —
   the same "this one is live now" cue the Framer prototype gives. The
   placeholder slides aside rather than sitting under the cursor. */
.bl-input:focus {
  outline: none;
  border-color: var(--bl-blue);
  box-shadow: 0 0 0 4px var(--bl-blue-soft), 0 6px 18px rgba(0, 0, 0, calc(0.28 * var(--bl-shadow-boost)));
  transform: translateY(-2px);
}

.bl-input:focus::placeholder {
  opacity: 0.55;
  transform: translateX(4px);
}

.bl-field:focus-within .bl-field__icon {
  color: var(--bl-blue);
  transform: translateY(-50%) scale(1.1);
}

/* Once a field has something in it the icon stays blue, so a filled form
   reads as progress at a glance. */
.bl-field:has(.bl-input:not(:placeholder-shown)) .bl-field__icon {
  color: var(--bl-blue);
}

@media (prefers-reduced-motion: reduce) {
  .bl-input, .bl-field__icon { transition: none; }
  .bl-input:focus { transform: none; }
  .bl-field:focus-within .bl-field__icon { transform: translateY(-50%); }
}

.bl-input[aria-invalid="true"] {
  border-color: var(--bl-danger);
  box-shadow: 0 0 0 4px rgba(217, 59, 59, 0.18);
}

/* Checkbox — a real input, visually restyled, so keyboard and screen
   reader behavior stay intact. */
.bl-check {
  display: flex;
  align-items: flex-start;
  gap: var(--bl-space-3);
  cursor: pointer;
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-medium);
  line-height: 1.35;
}

.bl-check input {
  appearance: none;
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin: 0.0625rem 0 0;
  border: 2px solid var(--bl-surface-raised);
  border-radius: var(--bl-radius-xs);
  background: var(--bl-surface-raised);
  cursor: pointer;
  transition: background-color var(--bl-dur-fast) var(--bl-ease-out),
              border-color var(--bl-dur-fast) var(--bl-ease-out);
}

/* Checked state is brand blue with a white tick, on both grounds. The
   yellow is the CTA's color and reserving it for the button keeps the one
   thing you want clicked visually unique. */
.bl-check input:checked {
  background-color: var(--bl-blue);
  border-color: var(--bl-blue);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E");
  background-size: 82%;
  background-position: center;
  background-repeat: no-repeat;
}

.on-light .bl-check input { border-color: var(--bl-gray-400); }

.bl-check a { text-decoration: underline; text-underline-offset: 2px; }

.bl-form__error {
  display: none;
  margin-top: var(--bl-space-2);
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  color: #ff8a8a;
}

.on-light .bl-form__error { color: var(--bl-danger); }

.bl-form__error[data-visible="true"] { display: block; }

.bl-form__note {
  font-size: var(--bl-size-micro);
  font-weight: var(--bl-weight-medium);
  line-height: 1.45;
  color: var(--bl-text-on-dark-muted);
  text-align: center;
}

.on-light .bl-form__note { color: var(--bl-text-muted); }

/* Honeypot. Off-screen rather than display:none — some bots skip hidden
   fields but happily fill positioned ones. */
.bl-hp {
  position: absolute !important;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   Floating menubar
   Used on every page except the homepage, which is deliberately nav-free.
   -------------------------------------------------------------------------- */

/* The menubar floats over the page, so any page that shows one has to leave
   room for it or the first heading disappears underneath. Set once here
   rather than per page template. */
.bl-has-nav .bl-page > main { padding-top: clamp(5.5rem, 4rem + 4vw, 7.5rem); }

/* PERSISTENT CHROME.

   The bar belongs to the SITE, not to the page. Naming it for the view
   transition takes it out of the root snapshot, so on an in-site navigation
   the content underneath fades and lifts while the bar simply stays where it
   is. That is the whole feel of the transition: the frame holds, the contents
   change. Without the name the bar was being cross-faded along with
   everything else, which is why the transition read as a flat flicker rather
   than as movement.

   The name has to be unique on the page; there is exactly one menubar. */
.bl-menubar { view-transition-name: bl-menubar; }

/* ...and it must not re-run its entrance on every navigation.

   Measured before this rule: arriving on a new page the bar ran its full
   520ms spring, translateY stepping -96 → -52 → -17 → -4 → +4 → +6 → +4 → 0.
   It overshoots by design, which is lovely once per visit and reads as a
   stutter when it happens on top of a 380ms page cross-fade, every single
   time you click a link. On a transitioned navigation it is already in
   place. */
.bl-vt .bl-menubar {
  transform: none;
  opacity: 1;
  transition: none;
}

/* Slides down on load, then stays put for the whole visit — it does not
   hide on scroll. The nav is the way back out of any page, so it should
   never be something you have to scroll up to retrieve. */
.bl-menubar {
  position: fixed;
  inset-inline: 0;
  top: var(--bl-space-4);
  z-index: var(--bl-z-menubar);
  padding-inline: var(--bl-gutter);
  transform: translateY(calc(-100% - var(--bl-space-6)));
  opacity: 0;
  transition: transform var(--bl-dur-slow) var(--bl-ease-spring),
              opacity var(--bl-dur-base) var(--bl-ease-out);
}

.bl-menubar.is-in {
  transform: none;
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .bl-menubar { transition: opacity var(--bl-dur-fast) linear; transform: none; }
}

/* Never leave the nav off-screen if JavaScript doesn't run. */
.no-js .bl-menubar { transform: none; opacity: 1; }

.bl-menubar__inner {
  display: flex;
  align-items: center;
  gap: var(--bl-space-5);
  max-width: var(--bl-max-width);
  margin-inline: auto;
  padding: 0.625rem 0.625rem 0.625rem 1.5rem;
  border-radius: var(--bl-radius-pill);
  background: var(--bl-surface-bar);
  box-shadow: var(--bl-shadow-menubar);
}

.bl-menubar__logo {
  display: flex;
  align-items: center;
  flex: none;
  margin-right: auto;
}

/* Sized so the lockup reads at roughly 55% of the pill height, matching the
   mockup. The logo files are cropped to their artwork — earlier versions had
   ~39% transparent padding baked in, which is why a 32px box rendered a 19px
   logo. */
.bl-menubar__logo img { height: 2.375rem; width: auto; }

.bl-menubar__nav {
  display: flex;
  align-items: center;
  gap: var(--bl-space-6);
}

.bl-navlink {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.5rem 0;
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.02em;
  /* Read from a token rather than a literal. The literal meant every page
     that joins the dark theme had to be remembered in a selector list in
     tokens.css, and the confirmation page was duly forgotten — its menubar
     sat near-black on a near-black bar. One token, redefined once inside the
     themed block, covers every such page including ones not built yet. */
  color: var(--bl-navlink-ink);
  background: none;
  border: 0;
  transition: color var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-navlink:hover,
.bl-navlink[aria-expanded="true"] { color: var(--bl-blue); }

.bl-navlink__chevron {
  width: 0.75rem;
  height: 0.75rem;
  transition: transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-navlink[aria-expanded="true"] .bl-navlink__chevron { transform: rotate(180deg); }

/* --------------------------------------------------------------------------
   Menubar CTA

   At rest it is a plain dark pill. On hover the dark shape shrinks from the
   left and settles into a circle at the right end, revealing the brand blue
   behind it, while the arrow fades in inside the disc and the button widens
   to make room. One element does the whole morph — the shape is a single
   absolutely-positioned box whose four insets animate, so the pill and the
   circle are the same object rather than a crossfade between two.
   -------------------------------------------------------------------------- */

.bl-navcta {
  --cta-h:    2.75rem;
  --cta-pad:  0.3125rem;                                  /* ring of blue around the disc */
  --cta-disc: calc(var(--cta-h) - (2 * var(--cta-pad)));  /* so the disc is a true circle */

  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  flex: none;
  height: var(--cta-h);
  padding-inline: 1.375rem;
  border-radius: var(--bl-radius-pill);
  background: var(--bl-blue);
  color: var(--bl-white);
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.01em;
  line-height: 1;
  white-space: nowrap;
  transition: padding-right var(--bl-dur-slow) var(--bl-ease-out);
}

.bl-navcta__shape {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  z-index: -1;
  border-radius: var(--bl-radius-pill);
  background: var(--bl-charcoal);
  transition:
    left   var(--bl-dur-slow) var(--bl-ease-out),
    top    var(--bl-dur-slow) var(--bl-ease-out),
    right  var(--bl-dur-slow) var(--bl-ease-out),
    bottom var(--bl-dur-slow) var(--bl-ease-out);
}

.bl-navcta__label { position: relative; }

.bl-navcta__arrow {
  position: absolute;
  top: 50%;
  right: var(--cta-pad);
  translate: 0 -50%;
  display: grid;
  place-items: center;
  width: var(--cta-disc);
  height: var(--cta-disc);
  color: var(--bl-white);
  opacity: 0;
  /* Held back slightly so the disc has formed before the arrow lands in it. */
  transition: opacity var(--bl-dur-fast) var(--bl-ease-out) 180ms;
}

.bl-navcta__arrow svg { width: 0.875rem; height: 0.875rem; }

.bl-navcta:hover,
.bl-navcta:focus-visible {
  padding-right: calc(var(--cta-pad) + var(--cta-disc) + 0.5rem);
}

.bl-navcta:hover .bl-navcta__shape,
.bl-navcta:focus-visible .bl-navcta__shape {
  left: calc(100% - var(--cta-disc) - var(--cta-pad));
  top: var(--cta-pad);
  right: var(--cta-pad);
  bottom: var(--cta-pad);
}

.bl-navcta:hover .bl-navcta__arrow,
.bl-navcta:focus-visible .bl-navcta__arrow { opacity: 1; }

.bl-navcta:active { transform: translateY(1px); }

@media (prefers-reduced-motion: reduce) {
  .bl-navcta,
  .bl-navcta__shape,
  .bl-navcta__arrow { transition: none; }
}

/* --------------------------------------------------------------------------
   Learn mega menu
   -------------------------------------------------------------------------- */

.bl-mega {
  position: absolute;
  top: calc(100% - 0.75rem);
  left: 50%;
  translate: -50% 0;
  z-index: var(--bl-z-megamenu);
  width: min(42rem, calc(100vw - 2 * var(--bl-gutter)));
  padding: var(--bl-space-7) var(--bl-space-6);
  border-radius: var(--bl-radius-xl);
  background: var(--bl-surface-raised);
  box-shadow: var(--bl-shadow-lg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.5rem);
  transition:
    opacity var(--bl-dur-base) var(--bl-ease-out),
    transform var(--bl-dur-base) var(--bl-ease-out),
    visibility 0s linear var(--bl-dur-base);
}

.bl-mega[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;
}

.bl-mega__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--bl-space-6);
}

.bl-mega__col + .bl-mega__col {
  padding-left: var(--bl-space-6);
  border-left: 1px solid var(--bl-border);
}

.bl-mega__heading {
  margin-bottom: var(--bl-space-5);
  font-size: var(--bl-size-xs);
  font-weight: var(--bl-weight-bold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bl-text-soft);
}

/* Breathing room between rows. The gap sits outside the hover pill so the
   pills stay distinct instead of running together. */
.bl-mega__col li + li { margin-top: 0.375rem; }

.bl-mega__item {
  display: flex;
  align-items: center;
  gap: var(--bl-space-4);
  padding: 0.75rem var(--bl-space-4);
  border-radius: var(--bl-radius-md);
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-medium);
  line-height: 1.3;
  color: var(--bl-text);
  transition: background-color var(--bl-dur-fast) var(--bl-ease-out),
              color var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-mega__item:hover,
.bl-mega__item:focus-visible { background: var(--bl-hover-fill); }

/* Every icon is normalized to a 24-unit square whose artwork is scaled to a
   constant optical size, so this fixed box renders them all consistently —
   see the note in assets/img/icons/README.md. */
.bl-mega__icon {
  display: grid;
  place-items: center;
  width: 1.375rem;
  height: 1.375rem;
  flex: none;
}

.bl-mega__icon img,
.bl-mega__icon svg { width: 100%; height: 100%; object-fit: contain; }

/* --------------------------------------------------------------------------
   Mobile navigation
   -------------------------------------------------------------------------- */

.bl-menubar__toggle {
  display: none;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--bl-text);
}

.bl-menubar__toggle span,
.bl-menubar__toggle span::before,
.bl-menubar__toggle span::after {
  display: block;
  width: 1.125rem;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform var(--bl-dur-menu) var(--bl-ease-spring),
              opacity var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-menubar__toggle span::before,
.bl-menubar__toggle span::after {
  content: "";
  position: absolute;
}

.bl-menubar__toggle span::before { transform: translateY(-6px); }
.bl-menubar__toggle span::after  { transform: translateY(6px); }

.bl-menubar__toggle[aria-expanded="true"] span { background: transparent; }
.bl-menubar__toggle[aria-expanded="true"] span::before { transform: rotate(45deg); }
.bl-menubar__toggle[aria-expanded="true"] span::after  { transform: rotate(-45deg); }

@media (max-width: 900px) {
  .bl-menubar__toggle { display: grid; }

  .bl-menubar__inner { padding-left: 1.25rem; }

  .bl-menubar__nav {
    position: absolute;
    inset-inline: var(--bl-gutter);
    top: calc(100% + var(--bl-space-3));
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    max-height: min(70vh, 34rem);
    overflow-y: auto;
    padding: var(--bl-space-4);
    border-radius: var(--bl-radius-xl);
    background: var(--bl-surface-raised);
    box-shadow: var(--bl-shadow-lg);

    /* Unfolds from the button that opened it rather than appearing in place.
       The origin is the top-right corner, which is where the toggle sits, so
       the panel reads as growing out of the thing you pressed. The scale is
       small on purpose — 0.97 is felt, not seen. */
    transform-origin: top right;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.625rem) scale(0.97);

    /* Closing is not opening played backwards. Opening wants a slow, soft
       landing — you are being shown something. Closing wants to be gone:
       shorter, and on an ease-in so it leaves immediately instead of
       hanging for a beat and then vanishing, which is what made the two
       feel like different animations. */
    transition: opacity 220ms var(--bl-ease-in-out),
                transform 220ms var(--bl-ease-in-out),
                visibility 0s linear 220ms;
  }

  .bl-menubar__nav[data-open="true"] {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity var(--bl-dur-menu) var(--bl-ease-out),
                transform var(--bl-dur-menu) var(--bl-ease-out),
                visibility 0s linear 0s;
  }

  /* The rows arrive after the panel, one behind the next. The delays live
     only on the open state, so closing takes everything away at once —
     a staggered exit makes a menu feel reluctant to leave. */
  .bl-menubar__nav > * {
    opacity: 0;
    transform: translateY(-0.4rem);
    /* No delay and barely any duration on the way out: the panel is already
       leaving, and rows that linger behind it are what reads as two things
       happening at once. */
    transition: opacity 120ms var(--bl-ease-in-out),
                transform 120ms var(--bl-ease-in-out);
  }

  .bl-menubar__nav[data-open="true"] > * {
    opacity: 1;
    transform: none;
    transition: opacity var(--bl-dur-base) var(--bl-ease-out),
                transform var(--bl-dur-base) var(--bl-ease-out);
  }

  .bl-menubar__nav[data-open="true"] > *:nth-child(1) { transition-delay: 110ms; }
  .bl-menubar__nav[data-open="true"] > *:nth-child(2) { transition-delay: 155ms; }
  .bl-menubar__nav[data-open="true"] > *:nth-child(3) { transition-delay: 200ms; }
  .bl-menubar__nav[data-open="true"] > *:nth-child(4) { transition-delay: 245ms; }
  .bl-menubar__nav[data-open="true"] > *:nth-child(5) { transition-delay: 290ms; }

  .bl-navlink {
    justify-content: space-between;
    width: 100%;
    padding: var(--bl-space-4) var(--bl-space-3);
    border-bottom: 1px solid var(--bl-border);
  }

  /* Learn is a button AND a panel. Left on the button, the rule drew itself
     between the two, so an open Learn had its own contents hanging below its
     own divider — the row looked like it had come apart. The wrapper carries
     the line instead, under the whole disclosure, and the button inside it
     gives its own up. */
  .bl-navitem { border-bottom: 1px solid var(--bl-border); }
  .bl-navitem > .bl-navlink { border-bottom: 0; }

  /* display:none cannot be animated, which is why Learn used to appear with
     no transition at all while everything around it moved. A grid row going
     0fr → 1fr animates to the content's own height without anyone measuring
     it, which is the same trick the FAQ accordion already uses here. */
  .bl-mega {
    position: static;
    translate: none;
    width: 100%;
    padding: 0;
    box-shadow: none;
    visibility: visible;
    transform: none;

    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    margin-block: 0;

    /* Not --bl-ease-out here, which is cubic-bezier(0.22, 1, 0.36, 1) — a
       quintic. It covers ~90% of the distance in the first quarter of the
       time, which on a height that is OPENING reads as a snap followed by a
       crawl. Played in reverse, closing, the same curve looks gentle, which
       is exactly the asymmetry: one curve, two completely different
       impressions. A symmetric ease-in-out moves the height evenly in both
       directions, which is what a panel unfolding should do. Opacity keeps
       the quintic — fades have no geometry to misread. */
    transition: grid-template-rows 380ms var(--bl-ease-in-out),
                margin-block 380ms var(--bl-ease-in-out),
                opacity var(--bl-dur-base) var(--bl-ease-out);
  }

  .bl-mega[data-open="true"] {
    grid-template-rows: 1fr;
    opacity: 1;
    /* Margin rather than padding on the grid child: margin is zero while the
       panel is closed, so a collapsed Learn reserves no space at all. Padding
       inside the clipped child was invisible but still something the layout
       had to reason about. */
    margin-block: var(--bl-space-3) var(--bl-space-5);
  }

  /* min-height:0 is what lets the row actually reach zero — a grid item's
     automatic minimum size is its content, so without this the panel
     collapses to nothing visible but keeps its full height in the layout. */
  .bl-mega__grid {
    grid-template-columns: 1fr;
    gap: var(--bl-space-5);
    min-height: 0;
    overflow: hidden;
  }

  .bl-mega__col + .bl-mega__col {
    padding-left: 0;
    padding-top: var(--bl-space-5);
    border-left: 0;
    border-top: 1px solid var(--bl-border);
  }

  /* Full width in the stacked mobile menu, with the label centered and the
     disc parked at the right edge. */
  .bl-menubar__cta {
    display: flex;
    justify-content: center;
    width: 100%;
    margin-top: var(--bl-space-4);
  }

  @media (prefers-reduced-motion: reduce) {
    .bl-menubar__nav,
    .bl-menubar__nav > *,
    .bl-mega,
    .bl-menubar__toggle span,
    .bl-menubar__toggle span::before,
    .bl-menubar__toggle span::after { transition: none; }

    .bl-menubar__nav > * { opacity: 1; transform: none; }
  }
}

/* --------------------------------------------------------------------------
   Accordion (FAQ)
   -------------------------------------------------------------------------- */

.bl-accordion {
  border-radius: var(--bl-radius-xl);
  background: var(--bl-surface-sunken);
  overflow: hidden;
}

.bl-accordion__item + .bl-accordion__item {
  border-top: 1px solid var(--bl-tint);
}

.bl-accordion__trigger {
  display: flex;
  align-items: center;
  gap: var(--bl-space-5);
  width: 100%;
  padding: var(--bl-space-5) var(--bl-space-6);
  border: 0;
  background: transparent;
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.015em;
  line-height: 1.35;
  text-align: left;
  color: var(--bl-text);
  transition: background-color var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-accordion__trigger:hover { background: var(--bl-tint-hover); }

/* The plus rotates into a cross — one element, two strokes, no icon swap. */
.bl-accordion__sign {
  position: relative;
  flex: none;
  width: 1rem;
  height: 1rem;
  color: var(--bl-text-faint);
}

.bl-accordion__sign::before,
.bl-accordion__sign::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  translate: 0 -50%;
  transition: transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-accordion__sign::after { transform: rotate(90deg); }

.bl-accordion__trigger[aria-expanded="true"] .bl-accordion__sign { color: var(--bl-black); }
.bl-accordion__trigger[aria-expanded="true"] .bl-accordion__sign::before { transform: rotate(45deg); }
.bl-accordion__trigger[aria-expanded="true"] .bl-accordion__sign::after  { transform: rotate(-45deg); }

.bl-accordion__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--bl-dur-base) var(--bl-ease-in-out);
}

.bl-accordion__panel[data-open="true"] { grid-template-rows: 1fr; }

.bl-accordion__panel > div { overflow: hidden; }

.bl-accordion__body {
  padding: 0 var(--bl-space-6) var(--bl-space-6);
  max-width: var(--bl-max-prose);
  font-size: var(--bl-size-sm);
  line-height: 1.6;
  color: var(--bl-text-muted);
}

.bl-accordion__body > * + * { margin-top: var(--bl-space-4); }

@media (max-width: 640px) {
  .bl-accordion__trigger { padding: var(--bl-space-4) var(--bl-space-5); }
  .bl-accordion__body { padding: 0 var(--bl-space-5) var(--bl-space-5); }
}

/* --------------------------------------------------------------------------
   Panel + footer reveal
   The light content panel scrolls over a fixed dark footer and lifts away
   from it with a rounded bottom edge.
   -------------------------------------------------------------------------- */

/* The panel always covers the viewport, however little content it holds.
   The footer is fixed to the bottom of the screen and sits behind this, so
   a panel shorter than the window leaves the footer showing before anyone
   has scrolled — on a 27" display a thin page exposed half of it. lvh
   rather than svh: on a phone the viewport grows when the URL bar retracts,
   and svh would leave a sliver of footer visible at that moment. */
/* And <main> fills whatever height that works out to.

   Without this the panel is taller than its content on any short page and
   the surplus is bare surface: the circuit ground is painted by <main>, so
   it stops where the words do and the bottom third of the panel goes flat
   white. On the confirmation page it also cut the confetti off mid-fall,
   since that canvas is sized to <main> as well. Stretching <main> rather
   than moving the ground onto the panel keeps one element responsible for
   the page's own background, which is what every page template already
   assumes. */
.bl-page {
  position: relative;
  z-index: var(--bl-z-panel);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100lvh;
  /* Tail room under every page, so the last thing on it is never flush
     against the bottom edge. It matters most on a phone and most of all for
     an anchored arrival — jump to #cookie-settings near the end of the
     Privacy Policy and, without this, the panel has already scrolled far
     enough to expose most of the footer behind it. That reads as the page
     having come apart rather than as a footer being revealed.

     Raised from 4rem to 7rem and then, on request, to 28rem. This is the
     panel's own tail, not a gap between two sections: the rounded bottom
     corners belong to it, and under a full-bleed band — an episode's dark
     CTA, say — a short tail reads as a stripe rather than as the page
     ending. At 28rem (448px) it reads as deliberate white space. One
     number; change it here and every page follows. */
  padding-bottom: 28rem;
  border-bottom-left-radius: var(--bl-radius-panel);
  border-bottom-right-radius: var(--bl-radius-panel);
  overflow: hidden;
  background: var(--bl-surface);
}

/* flex-grow only — never shrink, and size from content first, so a page
   longer than the viewport is completely unaffected by this. */
.bl-page > main { flex: 1 0 auto; }

/* No footer, nothing behind the panel: the rounded bottom is there to lift
   away from the footer, and with nothing under it the curve reads as a notch
   cut out of the page for no reason. Square it off.

   The min-height stays: the panel is what paints the page white, and the
   body behind it is black. */
.bl-no-footer .bl-page {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

.bl-footer {
  position: relative;
  z-index: var(--bl-z-footer);
  padding-block: var(--bl-space-9) var(--bl-space-6);
}

/* The reveal: the footer is pinned to the bottom of the viewport and the
   light page panel scrolls up and off it, lifting away at its rounded
   bottom edge. The slot reserves exactly the footer's height in flow so
   there is somewhere to scroll to.

   --bl-footer-h is measured in site.js. Without JavaScript the slot has no
   height, the footer stays in normal flow, and the page reads correctly —
   just without the reveal. */
.bl-footer-slot { position: relative; }

/* The reveal is enabled by JavaScript rather than a width breakpoint, via
   .bl-reveal-ok on <body>. The real constraint is not screen size — it is
   whether the footer fits inside the viewport. A fixed footer taller than
   the screen is anchored to the bottom, so its top would be pushed off and
   could never be scrolled to. site.js measures both and decides, which is
   why this works on a phone in landscape and bails out on a short one. */
@media (prefers-reduced-motion: no-preference) {
  .bl-has-reveal.bl-reveal-ok .bl-footer-slot {
    height: var(--bl-footer-h, auto);
  }

  .bl-has-reveal.bl-reveal-ok .bl-footer--reveal {
    position: fixed;
    inset: auto 0 0 0;
  }
}

.bl-footer__grid {
  display: grid;
  grid-template-columns: minmax(9rem, 1fr) repeat(2, minmax(8rem, 1fr)) auto;
  gap: var(--bl-space-7) var(--bl-space-6);
  align-items: start;
}

.bl-footer__logo img { width: clamp(6rem, 8vw, 8.25rem); }

.bl-footer__heading {
  margin-bottom: var(--bl-space-3);
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.02em;
}

.bl-footer__links li + li { margin-top: 0.375rem; }

/* No underline at rest keeps the two columns calm; it returns on hover and on
   keyboard focus so whatever is under the cursor is unmistakably a link and
   not a heading. The offset lifts the rule clear of the descenders in
   "Privacy Policy" and "Let's Connect". */
.bl-footer__links a {
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-regular);
  color: var(--bl-text-on-dark-muted);
  text-decoration: none;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
  transition: color var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-footer__links a:hover,
.bl-footer__links a:focus-visible {
  color: var(--bl-yellow);
  text-decoration: underline;
}


.bl-footer__cta { justify-self: end; align-self: center; }

.bl-footer__base {
  margin-top: var(--bl-space-8);
  padding-top: var(--bl-space-5);
  border-top: 1px solid var(--bl-hairline-dark);
  display: flex;
  /* nowrap is load-bearing. With wrap, any growth in the legal text — a
     longer company name, one more trademark line — pushes the icons onto
     their own row, where they sit under the fine print looking like
     something left behind rather than part of the footer. They belong at the
     right edge, in line with the text. Narrow screens get the wrap back
     below. */
  flex-wrap: nowrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--bl-space-7);
}

/* Fine print wants a measure as much as anything else does. Run full width
   on a wide display it becomes a 1,200px line at 11px, which nobody can
   track back to the left edge — and it left the icons stranded underneath.
   Capped, it reads as a block, and the space it gives up is exactly the
   space the icons need. */
.bl-footer__legal {
  flex: 0 1 58ch;
  min-width: 0;
  font-size: var(--bl-size-micro);
  line-height: 1.7;
  color: var(--bl-text-on-dark-muted);
}

/* The BrightLotus clarification runs on from the trademark notice rather
   than claiming a line of its own — three stacked lines of fine print made
   the base of the footer read as a wall. */
.bl-footer__legal span { display: inline; }

.bl-footer__social {
  display: flex;
  gap: var(--bl-space-4);
  flex: none;
  /* Lines up with the back-to-top button, which is pinned to the same edge. */
  margin-left: auto;
}

@media (max-width: 640px) {
  /* Below this there is not room for two columns of anything, so the icons
     drop under the fine print on purpose rather than by accident. */
  .bl-footer__base { flex-wrap: wrap; gap: var(--bl-space-5); }
  .bl-footer__legal { flex: 1 1 100%; }
  .bl-footer__social { margin-left: 0; }
}

.bl-footer__social a {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  color: var(--bl-text-on-dark-muted);
  transition: color var(--bl-dur-fast) var(--bl-ease-out),
              transform var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-footer__social a {
  position: relative;
}

.bl-footer__social a:hover,
.bl-footer__social a:focus-visible {
  color: var(--bl-yellow);
  transform: translateY(-2px);
}

/* Icon labels ----------------------------------------------------------------

   Three line icons in a footer are a guessing game: the mortarboard is not
   obviously a library of tutorials, and an @ is not obviously a newsletter.
   The label names the destination before the click rather than after it.

   It rides the same 2px lift the icon already does, so the two read as one
   gesture, and it appears on keyboard focus as well as hover — a tooltip only
   sighted mouse users can reach is decoration, not help. `title` would have
   been one attribute, but it waits a second, never shows on focus, and screen
   readers treat it inconsistently; this is a real element with real type. */

.bl-tip {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  left: 50%;
  z-index: var(--bl-z-toast);

  padding: 0.3rem 0.55rem;
  border-radius: var(--bl-radius-sm);
  border: 1px solid var(--bl-hairline-dark);
  background: rgba(16, 16, 18, 0.92);
  backdrop-filter: blur(6px);

  color: var(--bl-off-white);
  font-size: var(--bl-size-xs);
  font-weight: var(--bl-weight-medium);
  line-height: 1.2;
  letter-spacing: -0.01em;
  white-space: nowrap;

  opacity: 0;
  /* Starts a shade low and slightly small, so it arrives rather than blinks. */
  transform: translate(-50%, 4px) scale(0.96);
  transform-origin: bottom center;
  pointer-events: none;

  transition: opacity var(--bl-dur-fast) var(--bl-ease-out),
              transform var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-footer__social a:hover .bl-tip,
.bl-footer__social a:focus-visible .bl-tip {
  opacity: 1;
  transform: translate(-50%, 0) scale(1);
}

/* The last icon sits at the right edge on narrow screens, where a centered
   label would run off the page. */
@media (max-width: 640px) {
  .bl-footer__social a:last-child .bl-tip {
    left: auto;
    right: 0;
    transform: translate(0, 4px) scale(0.96);
    transform-origin: bottom right;
  }

  .bl-footer__social a:last-child:hover .bl-tip,
  .bl-footer__social a:last-child:focus-visible .bl-tip {
    transform: translate(0, 0) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .bl-tip { transition: opacity var(--bl-dur-fast) var(--bl-ease-out); transform: translate(-50%, 0); }
  .bl-footer__social a:hover .bl-tip,
  .bl-footer__social a:focus-visible .bl-tip { transform: translate(-50%, 0); }
  .bl-footer__social a:hover { transform: none; }
}

/* The icon set is black line art. Masking it with currentColor lets a single
   file work on both grounds and pick up the hover color, which a plain <img>
   can't do. */
.bl-icon-mask {
  display: block;
  width: 100%;
  height: 100%;
  background-color: currentColor;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
}

/* Below the four-across layout the blocks get explicit placement rather than
   being left to flow. Auto-flow put the logo and Learn on row one and then
   stranded Quick Links beside an empty quadrant, which is what made it look
   accidental. Named areas make the arrangement deliberate at every size. */
@media (max-width: 900px) {
  .bl-footer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-areas:
      "logo  cta"
      "learn quick";
    align-items: start;
  }

  .bl-footer__logo       { grid-area: logo; }
  .bl-footer__col--learn { grid-area: learn; }
  .bl-footer__col--quick { grid-area: quick; }

  .bl-footer__cta {
    grid-area: cta;
    justify-self: end;
    align-self: start;
  }
}

/* On a phone the CTA needs the full width to stay tappable, so it drops to
   its own row underneath the two link columns. */
@media (max-width: 640px) {
  .bl-footer { padding-block: 2.5rem 1.5rem; }

  .bl-footer__grid {
    grid-template-areas:
      "logo  logo"
      "learn quick"
      "cta   cta";
    gap: var(--bl-space-6) var(--bl-space-5);
  }

  .bl-footer__cta { justify-self: stretch; }
  .bl-footer__cta .bl-btn { width: 100%; }

  .bl-footer__base { margin-top: var(--bl-space-6); padding-top: var(--bl-space-4); }
  .bl-footer__logo img { width: 5.5rem; }

  /* "The Glow UP Tech Digest" is long for a half-width column — let it wrap
     tightly instead of stretching the row spacing. */
  .bl-footer__links a { line-height: 1.35; }
  .bl-footer__links li + li { margin-top: var(--bl-space-2); }
}

/* No single-column fallback for narrow phones: stacking Learn above Quick
   Links added ~150px and pushed the footer past the viewport, which switched
   the reveal off on exactly the devices where it was requested. Two columns
   hold up fine down to about 320px. */

@media (max-width: 520px) {
  .bl-footer__base { justify-content: flex-start; }
}

/* Tail room on a phone — but only tail room.

   Two rounds of this were spent trying to solve an anchored arrival with
   padding, which cannot work and made it worse: #cookie-settings is the last
   thing on the Privacy Policy, so scrolling it to the top of the viewport
   means scrolling past the end of the document. The browser clamps at the
   bottom, where the footer is fully revealed, and every rem of padding added
   is one more rem of white between the panel and the footer rather than one
   less. The real fix is in site.js, which centers a target that close to the
   end instead of topping it.

   This is now what it always should have been: enough room that the last
   paragraph of any page is not flush against the bottom edge. */
@media (max-width: 900px) {
  /* NOT 4x the desktop value. A flat 34rem is 544px, which is two thirds of
     an iPhone screen — the reader would thumb through most of a blank
     viewport to reach the footer. Tied to the viewport instead, so the tail
     is a fixed SHARE of the screen rather than a fixed number of pixels:
     ~236px on a 844px-tall phone, growing on a tablet, never below the
     8.5rem that was already there. */
  .bl-page { padding-bottom: clamp(8.5rem, 28vh, 28rem); }
}

/* --------------------------------------------------------------------------
   Entrance animation
   Elements marked [data-enter] start offset and settle into place, either on
   load (hero) or when scrolled into view. Honors reduced-motion by simply
   showing everything.
   -------------------------------------------------------------------------- */

/* Rise, fade, and sharpen. The blur is what makes this read as a Framer
   entrance rather than a plain fade — elements resolve into focus as they
   settle, which is why it's worth the extra property. */
[data-enter] {
  opacity: 0;
  transform: translateY(26px);
  filter: blur(7px);
  transition:
    opacity var(--bl-dur-enter) var(--bl-ease-enter),
    transform var(--bl-dur-enter) var(--bl-ease-enter),
    filter var(--bl-dur-enter) var(--bl-ease-enter);
  transition-delay: var(--enter-delay, 0ms);
  will-change: opacity, transform, filter;
}

/* The portrait scales up as it resolves, so it arrives with a bit more
   weight than the text around it. */
[data-enter="scale"] {
  transform: translateY(30px) scale(0.94);
  filter: blur(10px);
}

/* Opacity only. Used by the decoding headline, which resolves in place —
   a blur would fight the character animation, and a rise would drag the
   whole line around while it is still churning. */
[data-enter="fade"] {
  transform: none;
  filter: none;
  transition: opacity var(--bl-dur-enter) var(--bl-ease-enter);
  transition-delay: var(--enter-delay, 0ms);
}

/* A quieter arrival for elements that come in one after another — the
   signup rows. Measured off the reference: about 5px of downward settle
   over ~450ms, not the 26px rise the bigger blocks get. Anything more and
   rows 180ms apart visibly slide over each other. */
[data-enter="soft"] {
  transform: translateY(-6px);
  filter: blur(3px);
  transition:
    opacity var(--bl-dur-row) var(--bl-ease-enter),
    transform var(--bl-dur-row) var(--bl-ease-enter),
    filter var(--bl-dur-row) var(--bl-ease-enter);
  transition-delay: var(--enter-delay, 0ms);
}

[data-enter].is-entered {
  opacity: 1;
  transform: none;
  filter: none;
}

/* Arrived through a page transition? Then skip the per-element entrance.

   The browser's cross-document view transition has already faded and lifted
   the whole incoming page; running this on top of it made the page arrive
   twice. Only the above-the-fold set is suppressed — scroll-triggered
   entrances further down are not part of the arrival and still play.

   Deliberately CSS rather than script. The class lands on <html> from a
   pagereveal listener in head.php, and pagereveal fires AFTER the script at
   the end of <body> has already run — so a JavaScript check there tested a
   class that did not exist yet and silently did nothing. A selector has no
   such ordering problem: it applies the instant the class appears, and the
   elements are already at their final state before the first paint. */
.bl-vt [data-enter][data-enter-on="load"] {
  opacity: 1;
  transform: none;
  filter: none;
  transition: none;
}

/* Written on the fly ----------------------------------------------------

   The summary arrives a word at a time, as though it were being composed
   rather than fetched. Two problems come free with that idea and both are
   solved here rather than accepted:

   - A reader cannot read text that has not appeared yet. So the pace is set
     from the word count, not per word: the whole thing lands in about 1.2s
     whether the summary is twenty words or sixty, and a tap finishes it
     immediately for anyone who would rather just read it.
   - Words appearing one by one would normally reflow the panel on every
     line break, shoving the page around while someone is trying to read.
     So every word is laid out from the start at zero opacity — the box is
     its final size before the first word shows, and nothing moves.

   The real text stays in the markup and is what a screen reader announces;
   only its visibility is animated, so there is nothing to hear happening. */
[data-enter="gist"] {
  transform: none;
  filter: none;
  transition: opacity 180ms var(--bl-ease-out);
}

.bl-gist__w {
  display: inline-block;
  opacity: 0;
  transform: translateY(2px);
}

.bl-gist__w.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 260ms var(--bl-ease-out),
              transform 260ms var(--bl-ease-out);
}

/* If the script never runs, the words were never wrapped and this class
   never exists — the summary is simply there, which is the correct
   fallback and needs no rule of its own. */

/* Once an element has arrived, stop asking the compositor to keep a layer
   around for it. */
[data-enter].is-entered { will-change: auto; }

@media (prefers-reduced-motion: reduce) {
  [data-enter] {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }

  /* The veil has to go too, or the Gist keeps a set of grey bands across it
     forever — the one failure mode where honoring the setting would leave
     the page worse than the animation did. */
  .bl-gist__w { opacity: 1; transform: none; transition: none; }
}

/* No-JS safety net: never leave content invisible. */
.no-js [data-enter] { opacity: 1; transform: none; filter: none; }
.no-js .bl-gist__w { opacity: 1; transform: none; }


/* --------------------------------------------------------------------------
   Cookie notice

   A card in the bottom corner, not a bar across the page. The difference
   matters: a full-width bar reads as a toll gate and covers content on a
   phone, where the whole viewport is roughly one screen of article.

   Deliberately NOT a modal. Nothing behind it is blocked, focus is not
   trapped, and it can be ignored — the choice is remembered when they make
   it and the notice keeps its place until then.
   -------------------------------------------------------------------------- */

.bl-cookie {
  position: fixed;
  z-index: var(--bl-z-menubar);

  /* One inset, used on every side, so the card sits an equal distance from
     the left edge and the bottom edge. It did not before: the page gutter is
     a clamp that grows to 3.5rem on a wide screen, while the bottom was a
     flat 1.5rem — so the gap on the left was more than twice the gap below,
     and the card read as nudged rather than placed. A corner inset only
     looks deliberate when both measurements are the same number. */
  --bl-cookie-inset: var(--bl-space-5);

  left: var(--bl-cookie-inset);
  right: var(--bl-cookie-inset);
  bottom: var(--bl-cookie-inset);
  display: flex;
  /* Bottom LEFT, not centered.

     Centered, it reads as a modal — the thing you must deal with before the
     page — which is exactly the wrong weight for a question whose honest
     answer is "either is fine." In the corner it is a notice.

     Left rather than right for two reasons. Reading left-to-right, the eye
     ends each line on the left of the next one, so a card there is found
     without being hunted for, and it does not sit under the pointer's
     resting path on the right. And the menubar's one loud element — the
     signup button — is top right; putting this bottom left balances the page
     on the diagonal instead of stacking both demands in the same corner.

     Corner only once there is room for it: the mobile rule below puts it
     back across the full width, because a card hugging one edge of a
     390px screen is just a narrower card with a wasted margin. */
  justify-content: flex-start;
  pointer-events: none;          /* only the card itself is interactive */
}

.bl-cookie__card {
  pointer-events: auto;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--bl-space-3) var(--bl-space-4);
  align-items: start;
  width: min(100%, 34rem);
  padding: var(--bl-space-5) var(--bl-space-5) var(--bl-space-4);
  border: 1px solid var(--bl-border-strong);
  border-radius: var(--bl-radius-lg);
  background: var(--bl-surface-raised);
  color: var(--bl-text);
  box-shadow: 0 2px 6px rgba(0, 0, 0, calc(0.10 * var(--bl-shadow-boost))),
              0 18px 48px rgba(0, 0, 0, calc(0.18 * var(--bl-shadow-boost)));
}

.bl-cookie__emoji {
  margin: 0;
  font-size: 1.75rem;
  line-height: 1.1;
}

.bl-cookie__body { min-width: 0; }

.bl-cookie__title {
  margin: 0 0 var(--bl-space-2);
  font-size: var(--bl-size-lead);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.01em;
}

.bl-cookie__text {
  margin: 0;
  font-size: var(--bl-size-sm);
  line-height: 1.55;
  color: var(--bl-text-muted);
}

/* The copy is two paragraphs in the markup and always has been — what a
   cookie is, then what the two buttons do. A flat `margin: 0` was running
   them into one slab, so the structure the writing already had was invisible
   and the reader met six dense lines with no handhold.

   Sibling-only, so the first paragraph still sits tight under the heading;
   the gap belongs between the thought and the next thought, not above the
   first one. */
.bl-cookie__text + .bl-cookie__text {
  margin-top: var(--bl-space-3);
}

.bl-cookie__link {
  color: var(--bl-blue-deep);
  text-decoration: underline;
  text-underline-offset: 3px;
  white-space: nowrap;
}

/* The buttons sit under both columns so they can align right on a wide card
   and go full width on a narrow one. */
.bl-cookie__actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--bl-space-3);
  margin-top: var(--bl-space-2);
}

/* Arrival and exit.

   The entrance waits before it starts. That pause is the whole fix: the
   animation was always here, but at 280ms with no delay the card arrived in
   the same instant as the page itself, so it read as part of the load rather
   than as something asking a question — one more thing moving in a view that
   was already moving. Letting the page land first, then bringing the card up
   slowly, turns it into an interruption the reader notices and can finish
   reading before deciding.

   Six seconds, not the old fraction of one. Under a second still landed
   while the reader was orienting, so it got read as part of the page
   loading. Six is long enough that the page is finished, the eye has
   settled somewhere, and the card arriving is a separate event — something
   that turned up, which is what a notice should feel like. Short enough
   that it still turns up before a fast reader has left.

   The full slow duration, a slightly taller rise, and `both` so it stays
   invisible through the delay rather than flashing at full opacity and then
   animating.

   The exit is the same move in reverse, and the script removes the element
   when it finishes. */
.bl-cookie__card {
  animation: bl-cookie-in var(--bl-dur-slow) var(--bl-ease-out) 6000ms both;
}

/* `visibility` is in the keyframes for a reason, and it is not decoration.

   With `both`, the from-state holds through the whole delay, so for six
   seconds the card is opacity 0 — invisible, but still laid out, and still
   `pointer-events: auto`. An invisible box parked over the bottom-left
   corner would eat clicks on whatever is under it (footer links, mostly)
   for six seconds before anyone could see what hit them. At 800ms nobody
   would ever catch it; at six it is a real bug.

   `visibility: hidden` in the from-state takes the card out of hit testing
   entirely during the delay. Visibility animates discretely, and the rule
   is that if either endpoint is `visible` the element is visible for the
   whole duration — so it flips on the instant the animation starts and
   stays on, which is exactly the behavior wanted. */
@keyframes bl-cookie-in {
  from { opacity: 0; transform: translateY(16px); visibility: hidden; }
  to   { opacity: 1; transform: none;            visibility: visible; }
}

.bl-cookie.is-leaving .bl-cookie__card {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--bl-dur-fast) var(--bl-ease-out),
              transform var(--bl-dur-fast) var(--bl-ease-out);
  animation: none;
}

@media (max-width: 34rem) {
  .bl-cookie { justify-content: stretch; }
  .bl-cookie__card { grid-template-columns: 1fr; width: 100%; }
  .bl-cookie__emoji { font-size: 1.5rem; }
  .bl-cookie__actions { justify-content: stretch; }
  .bl-cookie__actions .bl-btn { flex: 1 1 auto; }
}

@media (prefers-reduced-motion: reduce) {
  /* Reduced motion means less movement, not less patience.

     The old rule dropped the animation and the delay together, so the card
     was present from the first paint. That was defensible when the delay
     was 800ms. It is not now: being there before the reader has finished
     landing is the exact complaint the six seconds exists to fix, and
     somebody with a vestibular sensitivity has no less claim to a page that
     holds still first.

     So the delay stays and the movement goes. Opacity alone is not the kind
     of motion the media query is about — no travel, no direction, nothing
     to track across the screen — and it avoids the jump-cut that a bare
     `animation: none` plus a delay would produce. Visibility still rides
     along so the invisible card cannot take clicks while it waits. */
  .bl-cookie__card { animation-name: bl-cookie-in-reduced; }

  @keyframes bl-cookie-in-reduced {
    from { opacity: 0; visibility: hidden; }
    to   { opacity: 1; visibility: visible; }
  }

  .bl-cookie.is-leaving .bl-cookie__card {
    transform: none;
    transition-duration: 1ms;
  }
}


/* ==========================================================================
   Back to top

   Only on the long documents — see includes/partials/to-top.php for why it is
   included per page rather than from the footer.

   Bottom RIGHT, deliberately. The cookie notice owns the bottom left on every
   page until it is answered, and two round things fading in at the same
   corner is a mess. Right is also where a thumb already is on a phone.
   ========================================================================== */

.bl-totop {
  position: fixed;
  right: var(--bl-space-5);
  bottom: var(--bl-space-5);
  z-index: var(--bl-z-toast);

  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;

  border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  border-radius: 50%;
  background: var(--bl-surface, #fff);
  color: var(--bl-text, #1d1f21);
  cursor: pointer;

  /* Enough shadow to lift it off a page of text, not so much that it reads
     as a floating action button demanding to be pressed. */
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08), 0 8px 24px rgb(0 0 0 / 0.10);

  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--bl-dur-base) var(--bl-ease-out),
              transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-totop.is-in {
  opacity: 1;
  transform: none;
}

.bl-totop:hover {
  background: color-mix(in srgb, currentColor 6%, var(--bl-surface, #fff));
}

/* 2.75rem is 44px, the minimum target size WCAG 2.2 asks for (2.5.8), so the
   button is already large enough and needs no padded hit area. */

@media (prefers-reduced-motion: reduce) {
  /* No travel, and no fade either — at this size a fade is just a slow
     appearance, and the whole point of the setting is that things stop
     sliding around. It is there or it is not. */
  .bl-totop { transition: none; transform: none; }
}

@media (max-width: 34rem) {
  .bl-totop {
    right: var(--bl-space-4);
    bottom: var(--bl-space-4);
  }
}
