/* VENDORED COPY, written by FirmForge/design-standard/generate_tokens.py.
   Do not edit here: change build_loading_css.mjs there, run it, then run
   generate_tokens.py, which rewrites every copy and --check holds them. */
/* ==========================================================================
   firmforge-loading.css — brand loading indicators
   --------------------------------------------------------------------------
   GENERATED by build_loading_css.mjs. Edit that, not this.

   The FirmForge mark is an iceberg cut by a waterline, so a rising and falling
   fill reads as loading without the mark ever having to spin.

     .ff-loader--tidal   flat water edge, floods and drains
     .ff-loader--swell   drifting waves on the water's surface

   Adopted 2026-10-20 from the MSP Dashboard's FirmForgeLoader (Tommy): the
   same design as before, redrawn. What changed, and why:

   - The mark is a vector traced from the 2000px master (99.86% agreement),
     embedded below. There is no PNG to serve and no path to set, so a vendored
     copy needs no local change, and nothing fails silently over file://.
   - Every animation is a transform, the standard's motion rule. The old Swell
     animated top and background-position, which stop while the page is busy:
     measured under a 700ms-per-second block, it froze for 833ms at a time,
     where this keeps moving (design-standard/loader-comparison.html).
   - The water's colour ramp rides the surface, so the crest is always the
     light ink, and the waves take their colour from the ink's properties
     rather than from a data URI restated per ink.

   Usage. THE TWO VARIANTS TAKE DIFFERENT MARKUP (unchanged).

     <span class="ff-loader ff-loader--tidal" role="status"
           aria-label="Loading"></span>

     <span class="ff-loader ff-loader--swell ff-loader--obsidian"
           style="--ff-loader-size:64px" role="status" aria-label="Loading">
       <span class="ff-loader__clip"><span class="ff-loader__water"></span></span>
     </span>

   Add .ff-loader--follow-theme to an obsidian loader that should switch to
   warm white under :root[data-theme="dark"].

   React apps can use the FirmForgeLoader component instead; it draws the same
   thing as inline SVG, and the two are checked against each other.
   ========================================================================== */

.ff-loader {
  --ff-loader-size: 48px;
  --ff-loader-mark: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%20100%20100'%20preserveAspectRatio%3D'none'%3E%3Cpath%20fill-rule%3D'evenodd'%20d%3D'M55.76%206.54L56.74%207.52L66.70%2022.95L65.53%2029.98L65.72%2030.18L66.31%2029.39L68.65%2025.10L73.73%2027.05L74.90%2027.64L79.20%2036.04L20.80%2036.04L30.18%2023.34L31.15%2023.34L41.89%2021.19L55.57%206.74ZM8.89%2038.57L91.11%2038.57L91.11%2039.94L79.00%2039.94L78.61%2040.33L71.58%2062.79L66.11%2061.62L65.33%2061.62L64.94%2062.01L58.69%2078.81L58.50%2079.00L54.20%2076.27L53.81%2076.66L47.95%2093.46L43.26%2079.00L42.09%2078.81L39.36%2079.39L37.99%2079.39L33.69%2063.57L27.05%2057.13L21.39%2040.14L21.19%2039.94L8.89%2039.94L8.89%2038.77ZM55.57%208.30L53.42%2024.71L44.82%2030.57L45.21%2030.76L56.74%2029.00L58.11%2029.00L62.40%2033.50L63.57%2035.06L25.29%2035.06L30.76%2024.32L41.50%2022.36L39.94%2028.61L40.14%2029.39L44.63%2019.82L55.37%208.50Z'%2F%3E%3C%2Fsvg%3E");

  /* ember: the brand default, and the only ink safe on any ground */
  --ff-loader-light: var(--color-ember-light, #F07C3A);
  --ff-loader-base:  var(--color-ember, #D4541A);
  --ff-loader-deep:  var(--color-ember-dark, #A83A0E);
  --ff-loader-crest: var(--color-text-inverse, #F8F4F0);
  --ff-loader-ghost: var(--color-ember-light, #F07C3A);
  --ff-loader-ghost-alpha: .20;

  /* One unit of the mark's 100-unit drawing, at the rendered size. */
  --ff-u: calc(var(--ff-loader-size) / 100);

  position: relative;
  display: inline-block;
  flex: none;
  width: var(--ff-loader-size);
  height: var(--ff-loader-size);
  /* The mark clips everything inside: the ghost, the water and the waves. */
  -webkit-mask: var(--ff-loader-mark) center / 100% 100% no-repeat;
          mask: var(--ff-loader-mark) center / 100% 100% no-repeat;
}

/* --------------------------------------------------------------------------
   Inks. The mono inks keep a narrow value ramp rather than one flat colour:
   without it the water surface has nothing to read against.
   -------------------------------------------------------------------------- */

.ff-loader--obsidian {
  --ff-loader-light: var(--color-obsidian-light, #1F2D40);
  --ff-loader-base:  var(--color-obsidian, #111827);
  --ff-loader-deep:  #0A0F18;
  --ff-loader-crest: var(--color-slate, #4A5568);
  --ff-loader-ghost: var(--color-obsidian, #111827);
  --ff-loader-ghost-alpha: .18;
}

.ff-loader--black {
  --ff-loader-light: #3A3A3A;
  --ff-loader-base:  #1A1A1A;
  --ff-loader-deep:  #000000;
  --ff-loader-crest: #5E5E5E;
  --ff-loader-ghost: #000000;
  --ff-loader-ghost-alpha: .16;
}

/* Warm white is the brand mark on dark grounds; plain white is for the cases
   that want pure white. Both invert the ramp: the crest is the brightest
   value, so the water under it sits below that value. */
.ff-loader--warm-white {
  --ff-loader-light: var(--color-text-inverse, #F8F4F0);
  --ff-loader-base:  #EAE4DC;
  --ff-loader-deep:  #C9C0B4;
  --ff-loader-crest: var(--color-text-inverse, #F8F4F0);
  --ff-loader-ghost: var(--color-text-inverse, #F8F4F0);
  --ff-loader-ghost-alpha: .22;
}

.ff-loader--white {
  --ff-loader-light: #FFFFFF;
  --ff-loader-base:  #EDEDED;
  --ff-loader-deep:  #C9C9C9;
  --ff-loader-crest: #FFFFFF;
  --ff-loader-ghost: #FFFFFF;
  --ff-loader-ghost-alpha: .22;
}

/* Opt-in: an obsidian loader on a ground that follows the page theme takes the
   warm-white ramp under a dark theme, where obsidian would be invisible.
   Opt-in so a loader on a fixed light panel inside a dark page stays obsidian. */
:root[data-theme="dark"] .ff-loader--obsidian.ff-loader--follow-theme {
  --ff-loader-light: var(--color-text-inverse, #F8F4F0);
  --ff-loader-base:  #EAE4DC;
  --ff-loader-deep:  #C9C0B4;
  --ff-loader-crest: var(--color-text-inverse, #F8F4F0);
  --ff-loader-ghost: var(--color-text-inverse, #F8F4F0);
  --ff-loader-ghost-alpha: .22;
}

/* --------------------------------------------------------------------------
   The un-filled berg, so the shape is always legible.
   -------------------------------------------------------------------------- */

.ff-loader::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ff-loader-ghost);
  opacity: var(--ff-loader-ghost-alpha);
}

/* --------------------------------------------------------------------------
   The water. Its top edge is the waterline; it is taller than the mark so
   there is always water below the line, and its colour ramp is measured from
   the line down, so the ramp travels with the surface.
   -------------------------------------------------------------------------- */

.ff-loader--tidal::after,
.ff-loader--swell .ff-loader__water {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: calc(var(--ff-u) * 220);
  will-change: transform;
}

/* --- Tidal: a flat edge with a bright crest band ------------------------- */

.ff-loader--tidal::after {
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ff-loader-crest) 95%, transparent) 0,
      transparent calc(var(--ff-u) * 11)),
    linear-gradient(180deg,
      var(--ff-loader-light) 0,
      var(--ff-loader-base) calc(var(--ff-u) * 45),
      var(--ff-loader-deep) calc(var(--ff-u) * 100));
  animation: ff-tide 2.6s cubic-bezier(.45, 0, .55, 1) infinite;
}

/* Water travel, from the top: 86 is nearly empty, 14 nearly full. */
@keyframes ff-tide {
  0%, 100% { transform: translateY(calc(var(--ff-u) * 86)); }
  50%      { transform: translateY(calc(var(--ff-u) * 14)); }
}

/* --- Swell: the surface is a wave, with a foam line drifting against it --- */

.ff-loader--swell .ff-loader__clip {
  position: absolute;
  inset: 0;
}

/* The body starts below the troughs; the wave strip above fills to the crest. */
.ff-loader--swell .ff-loader__water {
  background: linear-gradient(180deg,
    transparent 0,
    transparent calc(var(--ff-u) * 2.4),
    var(--ff-loader-light) calc(var(--ff-u) * 2.4),
    var(--ff-loader-base) calc(var(--ff-u) * 45),
    var(--ff-loader-deep) calc(var(--ff-u) * 100));
  animation: ff-tide 2.9s cubic-bezier(.45, 0, .55, 1) infinite;
}

.ff-loader--swell .ff-loader__water::before,
.ff-loader--swell .ff-loader__water::after {
  content: "";
  position: absolute;
  will-change: transform;
}

/* The body's surface: one wave period wide per tile, drifting one period. */
.ff-loader--swell .ff-loader__water::before {
  top: calc(var(--ff-u) * -2.4);
  left: calc(var(--ff-u) * -25);
  width: calc(var(--ff-u) * 150);
  height: calc(var(--ff-u) * 4.8);
  background: var(--ff-loader-light);
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%20-2.4%2025%204.8'%20preserveAspectRatio%3D'none'%3E%3Cpath%20d%3D'M0%200Q6.25%20-4.8%2012.5%200T25%200V2.4H0Z'%2F%3E%3C%2Fsvg%3E") 0 0 / calc(var(--ff-u) * 25) 100% repeat-x;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%20-2.4%2025%204.8'%20preserveAspectRatio%3D'none'%3E%3Cpath%20d%3D'M0%200Q6.25%20-4.8%2012.5%200T25%200V2.4H0Z'%2F%3E%3C%2Fsvg%3E") 0 0 / calc(var(--ff-u) * 25) 100% repeat-x;
  animation: ff-drift 2.1s linear infinite;
}

/* The foam: a shorter wave, stroked, drifting the other way so the two never
   resync into a visible beat. Started 6 units into its period, which is where
   the React component's path (drawn from x = -125) crosses the left edge. */
.ff-loader--swell .ff-loader__water::after {
  top: calc(var(--ff-u) * -3);
  left: 0;
  width: calc(var(--ff-u) * 134);
  height: calc(var(--ff-u) * 6);
  background: var(--ff-loader-crest);
  opacity: .55;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%20-3%2017%206'%20preserveAspectRatio%3D'none'%3E%3Cpath%20d%3D'M0%200Q4.25%20-3.2%208.5%200T17%200'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2.2'%20stroke-linecap%3D'round'%2F%3E%3C%2Fsvg%3E") calc(var(--ff-u) * -6) 0 / calc(var(--ff-u) * 17) 100% repeat-x;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%20-3%2017%206'%20preserveAspectRatio%3D'none'%3E%3Cpath%20d%3D'M0%200Q4.25%20-3.2%208.5%200T17%200'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2.2'%20stroke-linecap%3D'round'%2F%3E%3C%2Fsvg%3E") calc(var(--ff-u) * -6) 0 / calc(var(--ff-u) * 17) 100% repeat-x;
  animation: ff-drift-back 3.3s linear infinite;
}

@keyframes ff-drift      { from { transform: translateX(0); } to { transform: translateX(calc(var(--ff-u) * 25)); } }
@keyframes ff-drift-back { from { transform: translateX(0); } to { transform: translateX(calc(var(--ff-u) * -17)); } }

/* --------------------------------------------------------------------------
   Reduced motion. Hold a half-full frame rather than an empty berg: it still
   has to say "working", just without moving.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .ff-loader--tidal::after,
  .ff-loader--swell .ff-loader__water,
  .ff-loader--swell .ff-loader__water::before,
  .ff-loader--swell .ff-loader__water::after {
    animation: none;
  }
  .ff-loader--tidal::after,
  .ff-loader--swell .ff-loader__water {
    transform: translateY(calc(var(--ff-u) * 46));
  }
}
