/*
   Typing Headline component.

   Structure built by script.js:

     .com-typing-headline
       .type-effect
         ul.type-effect__source.screen-reader-text   real copy, AT + crawlers
         span.type-effect__line[aria-hidden]         visible, animated
           span.type-effect__text
           span.type-effect__cursor
         button.type-effect__toggle                  pause/play (WCAG 2.2.2)

   The source list is hidden by .screen-reader-text from
   assets/sass/07-utilities/a11y.scss — this file deliberately does not
   redefine it, and deliberately does NOT set list-style:none on it: Safari
   drops the list role when list styling is removed, which would undo the
   "list, N items" announcement the <ul> exists to produce.
*/

.com-typing-headline {
  --type-effect-cursor-color: currentColor;
  --type-effect-blink-speed: 1000ms;
}

/*
   Deliberately NOT position:relative. The pause control pins to the hero's
   corner, and a positioned .type-effect would become its containing block —
   which parks the button at the end of the typed text instead, riding along
   with the cursor. The hidden source is .screen-reader-text (clipped to 1px),
   so it needs no containing block of its own.
*/
.com-typing-headline:not(.is-in-hero) {
  position: relative;
}

.com-typing-headline .type-effect__line {
  display: inline;
}

.com-typing-headline .type-effect__text {
  /* The zero-width space script.js appends holds the line box open. */
  white-space: pre-wrap;
}

.com-typing-headline .type-effect__cursor {
  display: inline-block;
  color: var(--type-effect-cursor-color);
  animation: type-effect-blink var(--type-effect-blink-speed) step-end infinite;
}

/* Solid while characters are moving; it resumes blinking on every pause. */
.com-typing-headline .type-effect.is-typing .type-effect__cursor {
  animation: none;
  opacity: 1;
}

/*
   Paused and finished states must stop every moving thing, the blink included
   — a caret that blinks forever is still motion under WCAG 2.2.2, and once the
   rotation has stopped there is nothing left for the control to pause.
*/
.com-typing-headline .type-effect.is-paused .type-effect__cursor,
.com-typing-headline .type-effect.is-complete .type-effect__cursor {
  animation: none;
  opacity: 1;
}

@keyframes type-effect-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* Both selectors are needed: the query covers the OS preference, the attribute
   covers reducedMotion being forced on from PHP or JS. */
@media (prefers-reduced-motion: reduce) {
  .com-typing-headline .type-effect__cursor {
    animation: none;
    opacity: 1;
  }
}

.com-typing-headline .type-effect[data-motion='reduced'] .type-effect__cursor {
  animation: none;
  opacity: 1;
}

/* ------------------------------------------------------------- text case */

/*
   Uppercase is done here rather than by transforming the strings, so the DOM
   keeps the text as the editor wrote it: several screen readers spell all-caps
   words out letter by letter, and find-in-page and copy/paste both break on
   pre-uppercased text.

   Applied to the drawn line and the reserveSpace sizer only — never to
   .type-effect__source, which must stay exactly as entered for assistive tech
   and crawlers. The sizer needs it too, or it would measure lowercase glyphs
   and under-reserve the width.

   Title case has no usable CSS equivalent either: text-transform: capitalize
   capitalises every word (including "for", "a", "the") and leaves existing
   capitals alone, so it can neither follow editorial style nor fix all-caps
   input. script.js handles that one.
*/
.com-typing-headline .type-effect[data-text-case='uppercase'] .type-effect__line,
.com-typing-headline .type-effect[data-text-case='uppercase'] .type-effect__sizer {
  text-transform: uppercase;
}

/* ---------------------------------------------------------- pause control */

/*
   Deliberately quiet. WCAG asks for a mechanism (2.2.2), not a prominent one,
   and the constraints on going subtle are:

     2.5.8  target at least 24x24 CSS px      -> 32px here
     1.4.11 icon at least 3:1 on its backdrop -> see the alpha note below
     2.4.7  focus indicator must be visible   -> doubled ring
     1.4.3  4.5:1 for visible TEXT            -> N/A, the button is icon-only

   Taken out of the inline flow: as an inline box it sat after the animated
   text and slid along with every character drawn.
*/
.com-typing-headline .type-effect__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  color: hsl(0 0% 100%);
  /*
     The hero background is an editor-chosen photograph, so the icon cannot
     rely on it for contrast. Alpha 0.5 is the floor that still works over a
     pure-white image: 255 x (1 - 0.5) = ~#808080, which is 3.9:1 against a
     white icon. Anything lighter than ~0.42 drops under 1.4.11's 3:1.
  */
  background: hsl(0 0% 0% / 0.5);
  border: 1px solid hsl(0 0% 100% / 0.45);
  border-radius: 50%;
  cursor: pointer;
  text-shadow: none;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.com-typing-headline .type-effect__toggle:hover {
  background: hsl(0 0% 0% / 0.72);
  border-color: hsl(0 0% 100% / 0.9);
}

/* Doubled ring so focus reads on both light and dark photographs. */
.com-typing-headline .type-effect__toggle:focus-visible {
  outline: 3px solid hsl(0 0% 100%);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px hsl(0 0% 0% / 0.85);
}

@media (prefers-reduced-motion: reduce) {
  .com-typing-headline .type-effect__toggle { transition: none; }
}

/* Nothing is moving any more, so the control has nothing left to do. */
.com-typing-headline .type-effect.is-complete .type-effect__toggle {
  display: none;
}

/* Icons are drawn in CSS so the component ships no image assets. */
.com-typing-headline .type-effect__toggle-icon {
  display: block;
  width: 8px;
  height: 10px;
  border-left: 3px solid currentColor;
  border-right: 3px solid currentColor;
}

.com-typing-headline .type-effect.is-paused .type-effect__toggle-icon {
  width: 0;
  height: 0;
  border-left: 9px solid currentColor;
  border-right: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  margin-left: 2px;   /* optically centre the triangle in the circle */
}

/* ------------------------------------------------------- hero placement */

/*
   Hoisted into page-header. The custom properties are declared on
   .com-page-header, so they resolve here by inheritance and the headline
   tracks the hero title's fluid scale automatically. Fallbacks cover the
   in-flow case, where those properties do not exist.
*/
/*
   The inset must never reach zero. .com-page-header has no horizontal padding
   of its own (padding: 180px 0), so with align-left the headline sits flush
   against the viewport edge unless this padding holds it off. Centred heroes
   hide the problem, which is why the theme's own title can get away with
   dropping to padding: 0 on mobile.
*/
.com-typing-headline.is-in-hero {
  padding: 0 clamp(1.25rem, 4vw, 2rem);
  color: var(--color-com-page-header-text, hsl(0 0% 100%));
  font-size: var(--com-page-header-title-font-size, clamp(46px, 16px + 4vw, 64px));
  font-weight: bold;
  line-height: 1.15;
}

/*
   Pinned to the hero's bottom-right rather than sitting after the headline,
   so it never moves as characters are drawn or deleted. page-header only
   becomes a positioning context when a typing block is present, so no other
   page is affected.
*/
.com-typing-headline.is-in-hero .type-effect__toggle {
  position: absolute;
  right: clamp(0.75rem, 2vw, 1.5rem);
  bottom: clamp(0.75rem, 2vw, 1.5rem);
  margin: 0;
}

/* In flow there is no hero to pin to, so give it its own line below the
   headline — still out of the inline run, so it cannot trail the cursor. */
.com-typing-headline:not(.is-in-hero) .type-effect__toggle {
  margin-top: 1rem;
}

.com-typing-headline.is-in-hero .type-effect__text,
.com-typing-headline.is-in-hero .type-effect__cursor {
  text-shadow: 0 2px 12px hsl(0 0% 0% / 0.85), 0 0 40px hsl(0 0% 0% / 0.5);
}

/* In-flow fallback: no hero to inherit from, so set a self-contained scale. */
.com-typing-headline:not(.is-in-hero) {
  margin: 0 auto 2rem;
  padding: 0 2rem;
  max-width: 1200px;
  font-size: clamp(28px, 12px + 2.5vw, 44px);
  font-weight: bold;
  line-height: 1.2;
}

@media (width <= 800px) {
  .com-typing-headline.is-in-hero {
    /* Padding is fluid above and deliberately not reset here — zeroing it is
       what pushed left-aligned headlines flush against the edge. */
    max-width: 100%;
    line-height: 1.1;
  }
}

/* ------------------------------------------ empty subtitle in the hero */

/*
   The only rules in this file that reach outside the component, and they do so
   deliberately.

   page-header owns the hero, but this is a defect of the TYPED hero
   specifically, so the correction ships with the typed variant rather than
   being pushed into a component every other page depends on. It is doubly
   scoped: this stylesheet is enqueued only when the component renders, and
   .has-typed-headline only exists when page-header hoisted it. Neither
   condition holds on a hero without a typed headline, so the rules cannot
   reach one. Same pattern as editor-components/info-board/style.css.

   The defect: .com-page-header-subtitle is emitted unconditionally and keeps
   its spacing even with nothing in it. On a hero whose only visible line is the
   typed headline, that leaves 80px of dead wrapper below it, and the flex
   centring lands 40px high.

   :has(... :empty) is what protects a real subtitle — the wrapper collapses
   only when its .com-page-header-text holds nothing at all, so an editor who
   wrote one keeps every pixel of the original spacing. :empty has to target
   that inner element: the wrapper always contains whitespace text nodes around
   it and can never match :empty itself.

   Both properties are needed. Above 800px the hero pads the wrapper 40px;
   below, it zeroes the padding and uses margin: 2rem instead. Zeroing the pair
   is exactly what page-header already does for .is-text-hidden.

   Specificity is (0,5,0), clearing every other .com-page-header-subtitle rule
   in page-header/style.css and templates/editor-home-page/style.css, so this
   does not rely on which stylesheet the footer happens to print first.

   Two known limits, both no-ops rather than regressions:
   - :empty does not match a wrapper holding only whitespace. The home page's
     hero copy is a textarea, so a stray newline is the plausible case; that
     hero simply keeps the spacing it has today. Clearing the field fixes it.
   - A browser without :has() drops the rule whole and gets today's behaviour.
     Do not add unrelated declarations to these blocks — they would go down
     with it.
*/
.com-page-header.has-typed-headline .com-page-header-subtitle:has(.com-page-header-text:empty) {
  padding: 0;
  margin: 0;
}

/*
   hero_links renders immediately after the subtitle and sets margin: 0 auto,
   with no top margin of its own. The home page's uniform component spacing
   does not reach it either, because its previous sibling is
   .com-page-header-subtitle, which carries no .com class. So on a typed hero
   with no subtitle, that empty wrapper was the only thing holding the pills
   off the headline — collapsing it above would let them butt together.

   Hand the gap back explicitly, matching the home page's own rhythm.
*/
.com-page-header.has-typed-headline .com-page-header-subtitle:has(.com-page-header-text:empty) + .com-hero-links {
  margin-top: var(--editor-home-page-uniform-margin-vert, 2.4rem);
}

/*
   reserveSpace: stack the animated line over an invisible copy of EVERY line,
   all in the same grid cell. A grid cell takes the largest width and the
   largest height across its items, so the box is sized for the worst case in
   both dimensions and never resizes as the text changes — no measurement, and
   it re-resolves itself when the webfont loads or the type scale shifts.
   Editors reach it via "Reserve space for the longest line" in Advanced
   settings.
*/
.com-typing-headline .type-effect[data-reserve-space='true'] {
  display: grid;
  /*
     Items stretch to fill the cell (the grid default) so the hero's own
     text-align decides where each line sits. Pinning them with
     justify-items:start instead left short lines visibly off-centre in a
     centred hero, since the reserved box is as wide as the longest line.

     align-items:center keeps a short line in the middle of the reserved
     height rather than pinned to its top, so the text stays put vertically
     as lines wrap to different numbers of rows.
  */
  align-items: center;
}

.com-typing-headline .type-effect[data-reserve-space='true'] > .type-effect__sizer,
.com-typing-headline .type-effect[data-reserve-space='true'] > .type-effect__line {
  grid-area: 1 / 1;
}

.com-typing-headline .type-effect__sizer {
  visibility: hidden;
  pointer-events: none;
  user-select: none;
  white-space: pre-wrap;
}
