/* ============================================================================
   Expanding Ranks, the dot motif
   ----------------------------------------------------------------------------
   Load THIRD. Depends on 01-brand-tokens.css and 02-system-tokens.css.

   The mark is four staggered rows of circles widening downward, 4 then 5 then
   6 then 7, twenty two in total. This file turns that into a system rather
   than a logo: the same geometry does loading, progress, stage, count, empty,
   placeholder, divider and texture, so a person learns one shape once and then
   reads it everywhere.

   Three rules govern everything here.

   1. A DOT IS A UNIT OF ONE. It is never a decorative bullet and never a
      quantity it cannot represent. Twelve dots mean twelve. A number over
      about twelve gets a numeral, not more dots.
   2. FILLED AND EMPTY DIFFER BY SIZE AS WELL AS COLOUR. An unfilled dot is
      60 percent of a filled one. Nothing in this system is distinguished by
      colour alone, and the rank meter is the place that rule is easiest to
      break.
   3. THINGS BUILD, THEY DO NOT SLIDE. An element enters by rows stacking
      upward and dots landing outward from the centre of each row. Nothing
      slides in from off screen, because the mark does not and the motion has
      to look like the mark.

   Colour is not declared in this file. It is read from the roles in
   02-system-tokens.css, which is what makes the whole motif rebind correctly
   on the dark plate without a second set of rules.
   ========================================================================== */

/* ============================================================================
   1. THE DOT PRIMITIVE
   ========================================================================== */

.er-dot {
  display: block;
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--color-dot-filled);
  flex: none;
}

.er-dot--empty {
  width: calc(var(--dot) * var(--dot-empty-scale));
  height: calc(var(--dot) * var(--dot-empty-scale));
  background: var(--color-dot-empty);
}

/* The signal dot. This is the only decorative-looking use of Atomic Tangerine
   in the system and it is not decorative: it means this specific row is the
   one that needs a human. Section 8.2 of DESIGN.md reserves the colour for it. */
.er-dot--signal { background: var(--color-signal-mark); }
.er-dot--confirm { background: var(--color-confirm-mark); }

.er-dot--xs { width: var(--dot-xs); height: var(--dot-xs); }
.er-dot--sm { width: var(--dot-sm); height: var(--dot-sm); }
.er-dot--lg { width: var(--dot-lg); height: var(--dot-lg); }
.er-dot--xl { width: var(--dot-xl); height: var(--dot-xl); }
.er-dot--xs.er-dot--empty { width: calc(var(--dot-xs) * var(--dot-empty-scale)); height: calc(var(--dot-xs) * var(--dot-empty-scale)); }
.er-dot--sm.er-dot--empty { width: calc(var(--dot-sm) * var(--dot-empty-scale)); height: calc(var(--dot-sm) * var(--dot-empty-scale)); }
.er-dot--lg.er-dot--empty { width: calc(var(--dot-lg) * var(--dot-empty-scale)); height: calc(var(--dot-lg) * var(--dot-empty-scale)); }
.er-dot--xl.er-dot--empty { width: calc(var(--dot-xl) * var(--dot-empty-scale)); height: calc(var(--dot-xl) * var(--dot-empty-scale)); }

/* A row. Dots are centred on the row's own axis so filled and empty, which are
   different sizes, still sit on one line. */
.er-row {
  display: flex;
  align-items: center;
  gap: var(--dot-gap);
  min-height: var(--dot-lg);
}
.er-row--tight { gap: var(--dot-gap-xs); }
.er-row--loose { gap: var(--dot-gap-lg); }

/* ============================================================================
   2. RANK METER
   A count or a score, shown as filled units out of a total. This is the
   workhorse: the owner-candidate scan scores a person on six factors, a
   territory has three pipelines, a Journey has ten stages. All of them are
   small integers, which is exactly what a row of dots reads well.

   Markup carries the number in text for a screen reader; the dots are
   aria-hidden. A rank meter is never the only place a number appears.
   ========================================================================== */

.er-rank { display: inline-flex; align-items: center; gap: var(--space-2); }
.er-rank__dots { display: flex; align-items: center; gap: var(--dot-gap); }
.er-rank__value {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: var(--lead-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}
.er-rank--signal .er-rank__value { color: var(--color-signal-text); }

/* The fill animates one dot at a time, left to right, when the meter enters or
   its value changes. 90ms apart, so a six dot meter completes in 450ms and a
   twelve dot meter in 990ms. Anything longer than that wants a numeral. */
.er-js .er-rank--animate .er-dot { animation: er-dot-land var(--dur) var(--ease-out) backwards; }
.er-js .er-rank--animate .er-dot { animation-delay: calc(var(--i, 0) * var(--stagger-rank)); }

/* ============================================================================
   3. LOADING AND PROGRESS

   Indeterminate: the mark builds itself, over and over. Rows fill bottom to
   top, dots land outward from the centre of each row. It is the logo animation
   from the coming soon page, looped, at product scale.

   Determinate: a single row fills left to right. Use it whenever a real
   percentage exists, because a fake indeterminate spinner over a known
   quantity is a lie the user can feel.
   ========================================================================== */

.er-loader {
  display: inline-flex;
  flex-direction: column-reverse;  /* rows are authored bottom first, matching
                                      the build order, and reversed for paint */
  align-items: center;
  gap: var(--dot-gap);
}
.er-loader .er-row { justify-content: center; }
.er-loader .er-dot { background: var(--color-dot-filled); }

.er-js .er-loader .er-dot {
  animation: er-dot-pulse var(--dur-pulse) var(--ease-out) infinite;
  animation-delay: calc(var(--r, 0) * var(--stagger-pulse-row) + var(--i, 0) * var(--stagger-pulse-dot));
}

@keyframes er-dot-pulse {
  0%, 62%, 100% { opacity: 0.22; transform: scale(0.72); }
  16%           { opacity: 1;    transform: scale(1); }
}

/* Determinate. The track is the full set of dots; filled ones are the value.
   Markup is a real progressbar with aria-valuenow, and the numeral is present
   in text. Dots alone are never the whole story. */
.er-progress { display: flex; flex-direction: column; gap: var(--space-2); }
.er-progress__row { display: flex; align-items: center; gap: var(--dot-gap); }
.er-progress__label {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: var(--lead-sm);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

/* The continuous variant, for a value that is genuinely not countable in
   units, such as an import that reports bytes. It is a bar built out of the
   texture rather than a plain rectangle, so it still reads as ours. */
.er-meter {
  height: var(--dot);
  border-radius: var(--radius-pill);
  background: var(--color-track);
  overflow: hidden;
}
.er-meter__fill {
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--color-dot-filled);
  transition: width var(--dur-slow) var(--ease-out);
}
.er-meter--signal .er-meter__fill { background: var(--color-signal-mark); }

/* ============================================================================
   4. STAGE INDICATOR

   A pipeline has ten stages, eight on the main line and two off it. The
   indicator is a rank row with a rule through it: done stages are filled,
   the current one is a larger filled dot with a ring, upcoming are empty.

   The off-main-line stages, Rejected and Candidate Nurture, sit below the rule
   rather than on it, because they are not a later point in the same sequence
   and drawing them inline would say they were.
   ========================================================================== */

.er-steps { display: flex; flex-direction: column; gap: var(--space-3); }

.er-steps__line {
  display: flex;
  align-items: center;
  gap: 0;
  position: relative;
  /* The final stage label is absolutely positioned and does not wrap, so it
     extends past the last dot. Without this reserve the last stage name is
     clipped by the scroll container and reads as a broken layout rather than
     as something to scroll to. Measured against the real ten stage names. */
  padding-right: var(--space-16);
  padding-bottom: var(--space-6);
}

.er-step {
  display: flex;
  align-items: center;
  gap: 0;
  flex: 1 1 0;
  min-width: 0;
}
.er-step:last-child { flex: none; }

.er-step__dot {
  width: var(--dot-lg);
  height: var(--dot-lg);
  border-radius: 50%;
  background: var(--color-dot-empty);
  flex: none;
  position: relative;
  z-index: var(--z-raised);
}
.er-step--done .er-step__dot { background: var(--color-dot-filled); }
.er-step--current .er-step__dot {
  background: var(--color-dot-filled);
  box-shadow: 0 0 0 4px var(--color-surface), 0 0 0 6px var(--color-dot-filled);
}
.er-step--stalled .er-step__dot {
  background: var(--color-signal-mark);
  box-shadow: 0 0 0 4px var(--color-surface), 0 0 0 6px var(--color-signal-mark);
}

.er-step__rule {
  height: 2px;
  flex: 1 1 auto;
  min-width: var(--space-4);
  background: var(--color-divider);
}
.er-step--done .er-step__rule { background: var(--color-dot-filled); }

.er-step__label {
  position: absolute;
  top: calc(var(--dot-lg) + var(--space-2));
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  line-height: var(--lead-2xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}
.er-step--current .er-step__label { color: var(--color-text); font-weight: var(--wght-semibold); }
.er-step--stalled .er-step__label { color: var(--color-signal-text); font-weight: var(--wght-semibold); }

.er-steps__aside {
  display: flex;
  gap: var(--space-5);
  padding-top: var(--space-6);
  border-top: 1px solid var(--color-divider);
  margin-top: var(--space-6);
}

/* Vertical, for a narrow rail or a mobile viewport. The ten stage names do not
   fit on one line under 1024px and abbreviating them would break the
   vocabulary rule, so below that breakpoint the indicator turns. */
.er-steps--vertical .er-steps__line { flex-direction: column; align-items: flex-start; }
.er-steps--vertical .er-step { flex-direction: column; align-items: flex-start; width: 100%; flex: none; }
.er-steps--vertical .er-step__rule { width: 2px; height: var(--space-6); min-width: 0; margin-left: calc(var(--dot-lg) / 2 - 1px); }
.er-steps--vertical .er-step__label { position: static; padding-left: var(--space-3); white-space: normal; }
.er-steps--vertical .er-step > .er-step__label { margin-top: calc(var(--dot-lg) * -1); }

@media (max-width: 1024px) {
  .er-steps--responsive .er-steps__line { flex-direction: column; align-items: flex-start; }
  .er-steps--responsive .er-step { flex-direction: column; align-items: flex-start; width: 100%; flex: none; }
  .er-steps--responsive .er-step__rule { width: 2px; height: var(--space-6); min-width: 0; margin-left: calc(var(--dot-lg) / 2 - 1px); }
  .er-steps--responsive .er-step__label { position: static; padding-left: var(--space-3); white-space: normal; }
}

/* ============================================================================
   5. DIVIDERS AND TEXTURE
   ========================================================================== */

.er-divider {
  border: 0;
  height: 1px;
  background: var(--color-divider);
  margin: var(--space-8) 0;
}

/* A dotted rule. The dot spacing is the texture cell width halved, so a dotted
   divider and a textured panel are visibly the same grid. */
.er-divider--dots {
  height: 3px;
  background: radial-gradient(circle at center, var(--color-dot-empty) 1.4px, transparent 1.5px);
  background-size: calc(var(--texture-cell-x) / 2) 3px;
  background-repeat: repeat-x;
}

/* A divider that carries a label, for a long table or a day break in a
   timeline. The rule is dots on both sides. */
.er-divider-label {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: var(--space-8) 0;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  line-height: var(--lead-2xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.er-divider-label::before,
.er-divider-label::after {
  content: "";
  flex: 1 1 auto;
  height: 3px;
  background: radial-gradient(circle at center, var(--color-dot-empty) 1.4px, transparent 1.5px);
  background-size: calc(var(--texture-cell-x) / 2) 3px;
  background-repeat: repeat-x;
}

/* Background texture. The exact grid from the coming soon page. It is masked
   to an ellipse so it fades rather than ending on a hard edge, and it is
   aria-hidden decoration that never sits behind body copy, because a dot grid
   under text is a contrast problem that no ratio calculation will catch. */
.er-texture { position: relative; isolation: isolate; }
.er-texture::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-behind);
  pointer-events: none;
  background-image: radial-gradient(circle at center,
    color-mix(in srgb, var(--color-dot-empty) 45%, transparent) var(--texture-dot),
    transparent calc(var(--texture-dot) + 0.1px));
  background-size: var(--texture-cell-x) var(--texture-cell-y);
  mask-image: radial-gradient(ellipse 80% 70% at 50% 42%, #000 20%, transparent 78%);
}

/* ============================================================================
   6. AVATAR AND PLACEHOLDER

   A person with a name gets their initials. A person without one gets a dot
   cluster rather than a generic silhouette, because a silhouette of a human is
   a picture of a person who is not that person, and this product holds
   candidate records.
   ========================================================================== */

.er-avatar {
  width: var(--space-10);
  height: var(--space-10);
  border-radius: 50%;
  background: var(--color-surface-brand-tint);
  color: var(--color-brand);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-variation-settings: "wght" var(--wght-semibold), "wdth" var(--wdth-display);
  font-size: var(--text-sm);
  line-height: 1;
  letter-spacing: 0.02em;
  flex: none;
  overflow: hidden;
}
.er-avatar--sm { width: var(--space-6); height: var(--space-6); font-size: var(--text-3xs); }
.er-avatar--lg { width: var(--space-16); height: var(--space-16); font-size: var(--text-xl); }

/* The nameless placeholder: four dots in the mark's top-row arrangement. */
.er-avatar--anon { background: var(--color-surface-disabled); }
.er-avatar__cluster { display: flex; flex-wrap: wrap; width: calc(var(--dot-xs) * 2 + var(--dot-gap-xs)); gap: var(--dot-gap-xs); }
.er-avatar__cluster span {
  width: var(--dot-xs);
  height: var(--dot-xs);
  border-radius: 50%;
  background: var(--color-dot-empty);
}

/* A stack, for the recruiters on a territory. Overlap is negative margin, and
   the ring is the surface colour so it reads on any background it lands on. */
.er-avatar-stack { display: inline-flex; }
.er-avatar-stack .er-avatar { box-shadow: 0 0 0 2px var(--color-surface); }
.er-avatar-stack .er-avatar + .er-avatar { margin-left: calc(var(--space-3) * -1); }

/* ============================================================================
   7. SKELETON

   A loading placeholder shaped like the thing that is coming. Text skeletons
   are bars; count and score skeletons are dot rows, so the layout does not
   move when the real rank meter arrives.

   The shimmer is one of the two functional loops permitted in this system. It
   means work is happening right now and it stops when the work does.
   ========================================================================== */

.er-skeleton {
  background: var(--color-skeleton);
  border-radius: var(--radius-sm);
  position: relative;
  overflow: hidden;
}
.er-skeleton--text { height: var(--space-3); margin: var(--space-2) 0; }
.er-skeleton--title { height: var(--space-5); width: 44%; }
.er-skeleton--line-short { width: 62%; }
.er-skeleton--avatar { width: var(--space-10); height: var(--space-10); border-radius: 50%; }
.er-skeleton--block { height: var(--space-16); border-radius: var(--radius); }

.er-js .er-skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--color-skeleton-sheen), transparent);
  animation: er-shimmer var(--dur-shimmer) var(--ease) infinite;
}

@keyframes er-shimmer { to { transform: translateX(100%); } }

/* The rank skeleton is a real dot row at the empty state, so the moment the
   value lands the dots fill in place rather than the row being replaced. */
.er-skeleton-rank { display: flex; gap: var(--dot-gap); align-items: center; }

/* ============================================================================
   8. EMPTY STATE

   A partial mark: the bottom row only, half of it unfilled. It reads as a rank
   that has not been filled yet, which is exactly what an empty list is, and it
   is the one place the motif gets to carry the whole meaning.

   Every empty state names the thing that is missing in the product's own
   vocabulary and offers the action that fixes it. An empty state with no
   action is a dead end.
   ========================================================================== */

.er-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-4);
  padding: var(--space-16) var(--space-6);
  max-width: var(--measure-tight);
  margin-inline: auto;
}
.er-empty__mark { display: flex; gap: var(--dot-gap-lg); }
.er-empty__mark span {
  width: var(--dot-lg);
  height: var(--dot-lg);
  border-radius: 50%;
  background: var(--color-dot-empty);
  opacity: 0.55;
}
.er-empty__mark span:first-child,
.er-empty__mark span:nth-child(2) { background: var(--color-dot-filled); opacity: 1; }
.er-empty__title {
  font-family: var(--font-display);
  font-variation-settings: "wght" var(--wght-semibold), "wdth" var(--wdth-display);
  font-size: var(--text-xl);
  line-height: var(--lead-xl);
  color: var(--color-text);
  margin: 0;
}
.er-empty__body { color: var(--color-text-muted); margin: 0; }

/* A filtered-to-nothing list is a different situation from a genuinely empty
   one and gets different copy, so it gets its own class rather than the same
   component with different words passed in. */
.er-empty--filtered .er-empty__mark span { background: var(--color-dot-empty); opacity: 0.55; }

/* ============================================================================
   9. THE IDENTITY MARK

   Rebuilt in CSS from the same geometry as the SVG, so the animated mark and
   the asset cannot drift apart. Rows are authored bottom first, which is the
   order they build in, and reversed for paint.

   IDENTITY MOTION ONLY. 620ms per dot, rows 300ms apart, dots within a row
   45ms apart outward from the centre. It runs once per page load and it
   appears on the sign in screen, the full page loader and the marketing site.
   It never runs inside the application chrome.
   ========================================================================== */

.er-mark {
  display: inline-flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: var(--dot-gap-lg);
}
.er-mark .er-row { justify-content: center; gap: var(--dot-gap-lg); }
.er-mark .er-dot {
  width: var(--dot-xl);
  height: var(--dot-xl);
  background: var(--color-dot-filled);
}

/* Visible by default. The script adds er-js to <html> and only then is
   anything hidden, so a blocked or failed script leaves a complete mark rather
   than a blank space. Never invert this. */
.er-js .er-mark--animate .er-dot {
  opacity: 0;
  transform: scale(0.2) translateY(var(--dot-rise));
  animation: er-dot-build var(--dur-dot) var(--ease-out) forwards;
  animation-delay: calc(var(--r, 0) * var(--stagger-row) + var(--i, 0) * var(--stagger-lateral));
}

@keyframes er-dot-build {
    0% { opacity: 0; transform: scale(0.2) translateY(var(--dot-rise)); }
   62% { opacity: 1; transform: scale(var(--dot-overshoot)) translateY(0); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

/* The product-scale version of the same landing, used by the rank meter and by
   anything that gains a dot. Shorter, no overshoot past the product ceiling. */
@keyframes er-dot-land {
    0% { opacity: 0; transform: scale(0.4); }
  100% { opacity: 1; transform: scale(1); }
}

/* The wordmark. Hierarchy comes from weight and width, never from colour.
   Do not recolour either word and do not put Atomic Tangerine in it. */
.er-logotype {
  font-family: var(--font-display);
  text-transform: uppercase;
  color: var(--color-brand);
  line-height: 0.95;
  margin: 0;
}
.er-logotype span { display: block; }
.er-logotype__expanding {
  font-variation-settings: "wght" var(--logotype-expanding-weight), "wdth" var(--logotype-expanding-width);
  letter-spacing: var(--logotype-expanding-track);
}
.er-logotype__ranks {
  font-variation-settings: "wght" var(--logotype-ranks-weight), "wdth" var(--logotype-ranks-width);
  letter-spacing: var(--logotype-ranks-track);
  font-size: 1.32em;
}

/* ============================================================================
   10. ENTRANCE MOTION

   The vocabulary, generalised. Two shapes and no third.

   BUILD   a set of units appearing, outward from the centre of each row and
           bottom row to top. Used by the mark, the loader, a rank meter
           filling, a set of cards arriving.
   RISE    a single block of content settling into place from 10px below.
           Used by a panel, a row, a toast, a modal.

   There is no slide, no fade from the side, and no scale from zero. Nothing in
   the physical world appears from nothing, and nothing in this product arrives
   from off screen, because the mark does neither.
   ========================================================================== */

.er-reveal { }   /* visible by default. The rule below is what hides it */

.er-js .er-reveal {
  opacity: 0;
  transform: translateY(var(--reveal-distance));
  transition: opacity var(--dur-reveal) var(--ease-reveal),
              transform var(--dur-reveal) var(--ease-reveal);
  transition-delay: calc(var(--row, 0) * var(--stagger-reveal));
}
.er-js .er-reveal.is-revealed { opacity: 1; transform: none; }

/* Anything already past the middle of the viewport when its turn arrives does
   not animate at all. The visitor got there first, so the element is simply
   there. er-motion.js sets this. */
.er-js .er-reveal.is-instant { transition: none; opacity: 1; transform: none; }

/* ============================================================================
   REDUCED MOTION, mechanism 2 of 3

   Mechanism 1 in 02-system-tokens.css collapses durations and delays. It does
   NOT stop an element being stuck in its hidden start state, because
   opacity: 0 is not a duration. This block forces the end state on every class
   in this file that starts hidden. Mechanism 3 is in er-motion.js.

   Anything added later that starts at opacity 0 has to be added here in the
   same commit. A blank page is the failure mode and it is invisible under
   normal conditions, so it will not be caught by looking.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .er-js .er-mark--animate .er-dot,
  .er-js .er-rank--animate .er-dot,
  .er-js .er-reveal {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }
  /* The loader pulse frozen at one frame is a ring with dots missing, which
     reads as a rendering fault rather than as busy. Hold every dot at a legible
     opacity instead. The state is carried by aria-busy and the visible label,
     never by the motion. */
  .er-js .er-loader .er-dot { animation: none !important; opacity: 0.55; transform: none; }
  .er-js .er-skeleton::after { animation: none !important; opacity: 0; }
}
