/* Named things. Every rule here is reached through a class.

   The reuse rule: a component never sets its own outer margin. The parent
   spaces its children. That is what lets a component move between contexts
   without being edited. */

@layer components {
  /* First thing in the tab order: lets a keyboard user jump the nav. Offscreen
     until focused, never display:none — that would remove it from tab order
     entirely and defeat the point. */
  .skip-link {
    position: absolute;
    left: -9999px;
  }

  .skip-link:focus {
    left: var(--space-s);
    top: var(--space-s);
    z-index: 10;
    background-color: var(--base);
    color: var(--ink);
    padding: var(--space-xs);
    border: 2px solid var(--accent);
  }

  /* Wordmark — navigation, not document structure, so it is an <a> and not an
     <h1>. The pixel daffodil is the mark on every page; the name beside it
     appears on / only. aria-label names the link the same everywhere. */
  .wordmark {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    min-block-size: var(--tap);
    font-size: var(--text-brand);
    color: var(--ink);
    text-decoration: none;
  }

  /* 25×38 at 1 CSS px per sprite pixel: whole device pixels at any DPR, so
     the pixel art stays crisp. Never scale it by a fraction. */
  .wordmark img {
    display: block;
  }

  /* Groups the wordmark with its tagline on / only: the link's row, then the
     tagline. The group spaces the two (the reuse rule), so neither carries a
     margin of its own. */
  .wordmark-group {
    display: grid;
    justify-items: start; /* the link keeps its own width, not the tagline's */
    gap: var(--space-2xs);
  }

  .wordmark-tagline {
    font-size: var(--text-meta);
    color: var(--muted);
  }

  /* Theme toggle — absent without JS (the html.js gate): a toggle with no
     working click handler yet must be hidden, not just disabled. The two
     pixel icons sit stacked in the same spot and cross-fade on [data-theme];
     only one is ever visible per theme. Fade only, no rotation — a pixel
     sprite blurs at any angle that isn't a multiple of 90deg. The label
     stays fixed text (visually hidden below) and aria-pressed carries the
     state; swapping textContent as well would duplicate the job. */
  .theme-toggle {
    display: none;
  }

  html.js .theme-toggle {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: var(--tap);
    min-block-size: var(--tap);
    padding: 0;
    background: none;
    border: 1px solid var(--rule);
    color: var(--ink);
    cursor: pointer;
    transition: border-color var(--motion-fast);
  }

  html.js .theme-toggle:hover {
    border-color: var(--accent);
  }

  /* 12×12 sprite at 2 CSS px per sprite pixel — a whole-number scale, so
     crispEdges rendering stays on device-pixel boundaries. Both icons share
     one fill token: each is only ever visible in the theme that token suits
     (terracotta in light, its dark-mode peach in dark), so one declaration
     keeps both on-palette without picking two colours by hand. */
  .theme-toggle__icon {
    position: absolute;
    inset: 0;
    margin: auto;
    inline-size: calc(var(--tap) / 2);
    block-size: calc(var(--tap) / 2);
    fill: var(--accent);
    opacity: 0;
    transition: opacity var(--motion-fast);
  }

  /* Resting default: light, sun shown, moon hidden (moon inherits opacity: 0
     from .theme-toggle__icon above). */
  .theme-toggle__icon--sun {
    opacity: 1;
  }

  /* No explicit choice stored yet — defer to the OS preference. */
  @media (prefers-color-scheme: dark) {
    html:not([data-theme]) .theme-toggle__icon--sun {
      opacity: 0;
    }

    html:not([data-theme]) .theme-toggle__icon--moon {
      opacity: 1;
    }
  }

  /* An explicit choice always wins, OS preference or not. Explicit light
     needs no rule of its own: it matches neither selector above, so both
     icons fall back to their resting default. */
  html[data-theme="dark"] .theme-toggle__icon--sun {
    opacity: 0;
  }

  html[data-theme="dark"] .theme-toggle__icon--moon {
    opacity: 1;
  }

  /* Visually hidden but present in the accessibility tree — used for the
     toggle's fixed text label, kept off-screen now that the icon carries the
     meaning at a glance. Clipped rather than display:none, which would drop
     it from the accessible name entirely. */
  .visually-hidden {
    position: absolute;
    overflow: hidden;
    inline-size: 1px;
    block-size: 1px;
    white-space: nowrap;
    clip-path: inset(50%);
  }

  /* Metadata — dates, tags, asides. Never a heading. */
  .meta {
    font-size: var(--text-meta);
    color: var(--muted);
  }

  .meta-tags::before {
    content: " · ";
  }

  /* Verse. The <br>s are the author's line breaks; a line too long for a narrow
     screen still wraps, but the hanging indent makes the wrap read as overflow
     rather than as a new line of the poem. */
  .poem {
    font-style: italic;
    padding-inline-start: var(--space-s);
    text-indent: calc(var(--space-s) * -1);
  }

  /* Post summaries. The layer order is doing the work here: this overrides
     layout's `article > * + *` without a more specific selector, because
     components is declared after layout. */
  .post-summary > * + * {
    margin-block-start: var(--space-2xs);
  }

  .post-summary h2 a {
    text-decoration: none;
  }

  .post-summary h2 a:hover {
    text-decoration: underline;
  }

  /* Tag list and pagination — rows of links that wrap. */
  .tag-list ul,
  .pagination {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-s);
    list-style: none;
    padding: 0;
    align-items: center;
  }

  .tag-list li + li {
    margin-block-start: 0;
  }

  .tag-list h2 {
    font-size: var(--text-meta);
    color: var(--muted);
    font-weight: 400;
  }

  .pagination {
    justify-content: space-between;
    border-block-start: 1px solid var(--rule);
    padding-block-start: var(--space-s);
  }

  /* The disabled ends of the pager. Muted rather than removed, so the control
     keeps its shape from page to page. */
  .pagination span[aria-hidden] {
    color: var(--muted);
  }

  .pagination-position {
    font-size: var(--text-meta);
    color: var(--muted);
  }

  /* Hero — home page only: greeting beside the circle.
     Wider than --measure because the circle needs the room; prose still wraps
     at the measure inside <main>. Its width comes from <main> (layout.css). */
  .hero {
    display: grid;
    /* minmax(0, …) rather than a bare 1fr. An fr track floors at min-content,
       and the circle's width is min(100%, …) — a percentage that cannot resolve
       while the track is still being sized, so it falls back to the raw calc
       and inflates the column past the viewport. Flooring at 0 makes the track
       definite first, and the percentage then resolves against it. */
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-l);
    justify-items: center;
    align-items: center;
  }

  /* The parent places its children (the reuse rule): the circle stays centred
     in its track even when the desktop split starts the greeting at the edge. */
  .hero > .circle {
    justify-self: center;
  }

  @media (min-width: 45rem) {
    .hero {
      /* The brief's split: greeting on a third, circle on two thirds. */
      grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
      justify-items: start;
    }
  }

  /* Typewriter */

  .typewriter {
    --ch: 13; /* "hi, i'm sarah" — the only number to change */
    display: inline-block; /* width does nothing on an inline element */
    inline-size: calc(var(--ch) * 1ch);
    font-family: var(--font-mono);
    /* The page's h1, so it takes the title size — at section size it tied with
       the h2 below it and the wordmark, and / had no hierarchy. */
    font-size: var(--text-title);
    border-right: 2px solid var(--ink);
    white-space: nowrap;
    overflow: hidden;
    /* The caret waits for the typing to finish (the 3.5s delay), blinks four
       times, then rests solid. WCAG 2.2.2: anything that blinks on its own must
       stop within 5s — 4 × 0.8s is 3.2s. */
    animation:
      type 3.5s steps(var(--ch)) 1,
      blink 0.8s step-end 3.5s 4;
  }

  /* No `to`: an omitted endpoint defaults to the element's own computed width,
     which removes the third place --ch would otherwise be repeated. */
  @keyframes type {
    from {
      inline-size: 0;
    }
  }

  @keyframes blink {
    50% {
      border-color: transparent;
    }
  }

  /* Circle nav */

  .circle {
    --count: 4;
    --span: 360deg;
    --offset: -90deg; /* item 0 at the top */
    --radius: clamp(5.5rem, 28vmin, 10rem);
    /* Allowance for the widest label ("Favorites" ≈ 85px). At the 5.5rem floor
       this keeps the whole ring inside a 320px viewport: 2 × 88 + 96 = 272. */
    --label: 6rem;
    border: 1px dashed var(--rule);
    border-radius: 50%;

    position: relative;
    /* The nav points are text, so the box is 2 × radius + widest label.
       Sizing it for a dot would clip the labels; min() keeps it on screen. */
    inline-size: min(100%, calc(var(--radius) * 2 + var(--label)));
    aspect-ratio: 1;
  }

  .circle ul {
    list-style: none;
    padding: 0;
  }

  .circle li {
    --angle: calc(var(--offset) + var(--span) / var(--count) * var(--i));
    position: absolute;
    top: 50%;
    left: 50%;
    margin: 0;
    /* centre on the midpoint, aim, push out to the rim, then un-rotate so the
       label stays level */
    transform: translate(-50%, -50%) rotate(var(--angle))
      translate(var(--radius)) rotate(calc(-1 * var(--angle)));
  }

  /* Positioning lives on the <li>, scaling on the <a>. A transform on hover
     would otherwise replace the positioning transform and fling the item to
     the centre. */
  .circle a {
    display: grid;
    place-items: center;
    min-block-size: var(--tap);
    min-inline-size: var(--tap);
    padding-inline: var(--space-2xs);
    text-align: center;
    transition: scale var(--motion-fast);
  }

  .circle a:hover,
  .circle a:focus-visible {
    scale: 1.15;
  }

  /* Nav-link colour — shared by the header nav and the circle nav on /, so
     both read as the same kind of control. Nav links sit back until you
     reach for them: in accent they carried the same weight as the page
     title and the wordmark, so three things competed to be looked at first
     and none of them won. */
  .nav-items a,
  .circle a {
    color: var(--muted);
  }

  .nav-items a:hover,
  .nav-items a[aria-current="page"],
  .circle a:hover,
  .circle a[aria-current="page"] {
    color: var(--accent);
  }

  .nav-items a:hover {
    text-decoration: underline;
  }

  /* The circle sits inside <main>, so without this it would inherit main's
     underline-every-link rule. It never underlines — its own affordance is
     the scale above, and an underline mid-orbit would fight the curve. This
     also leaves [aria-current="page"]'s underline (base.css, global) free to
     apply to .nav-items a on its own specificity, unshadowed by a rule here. */
  .circle a {
    text-decoration: none;
  }

  /* Hold-to-fill heart — the centre of the circle */

  .hold {
    position: absolute;
    top: 50%;
    left: 50%;
    translate: -50% -50%;
    /* The clear space inside the ring: the left and right labels each reach
       --label/2 inwards from the rim, so anything wider than this collides
       with them. Derived rather than guessed, so it holds at every radius.
       The extra --space-s keeps the blurb from sitting flush against a label —
       without it the boxes meet exactly at the rim. */
    inline-size: calc(var(--radius) * 2 - var(--label) - var(--space-s));
    text-align: center;
    /* Spaces and centres its own children, so the blurb and the heart carry
       no margins of their own. */
    display: grid;
    justify-items: center;
    gap: var(--space-2xs);

    touch-action: manipulation; /* keeps scrolling; kills the 300ms tap delay */
    user-select: none;
    -webkit-touch-callout: none; /* suppresses the iOS long-press menu */
  }

  .hold p {
    font-size: var(--text-meta);
    color: var(--muted);
  }

  .heart {
    display: block;
    inline-size: clamp(3rem, 10vmin, 4.5rem);
    shape-rendering: crispEdges; /* keeps the pixel edges hard */
  }

  .heart-empty {
    fill: var(--rule);
    /* The only affordance on touch, where there is no hover to hint with.
       Three pulses (4.8s), not forever: WCAG 2.2.2 caps unprompted motion at 5s. */
    animation: pulse 1.6s ease-in-out 3;
  }

  /* steps(10) is the whole trick: the heart is 10 pixel rows tall, so the fill
     climbs one row at a time rather than sliding. The mechanism and the pixel
     identity agree. */
  .heart-fill {
    fill: var(--accent);
    clip-path: inset(100% 0 0 0);
    transition: clip-path var(--motion-slow) steps(10);
  }

  [data-hold-state="held"] .heart-fill {
    clip-path: inset(0 0 0 0);
  }

  @keyframes pulse {
    50% {
      opacity: 0.5;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .typewriter,
    .heart-empty {
      animation: none;
    }
  }
}
