/* Reaction bar — the love this / hate this sigils.
   Glyph geometry lives in js/sigils.js; everything here is state + motion. */

.reactions {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

/* No pill. An earlier pass gave these the red-tint chip treatment used by
   .post__share button and the tag pills, on the grounds that a bare glyph
   did not say "press me". That reasoning has since expired: the glyphs now
   beat, rattle, spark and smoke on hover, which is a far stronger
   affordance than a border ever was, and chip chrome around an 18-22px
   mark reads as chunky beside the writing it belongs to.

   The border box is kept at transparent rather than removed. The
   reduced-motion failure state needs an edge to flash, and keeping the box
   means nothing shifts by a pixel when it does. */
.reaction {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.35rem;
  background: none;
  border: 1px solid transparent;
  border-radius: 999px;
  color: var(--color-text-label);
  cursor: pointer;
  opacity: 0.85;
  transition: opacity var(--transition-fast),
              color var(--transition-fast),
              border-color var(--transition-fast);
}

.reaction:hover:not(:disabled) {
  opacity: 1;
  color: var(--color-red-bright);
}

/* The visitor's own vote. With no pill this rests entirely on colour plus
   the glyph's own glow, which is also why the count turns red. */
.reaction.is-on {
  opacity: 1;
  color: var(--color-red-bright);
}

.reaction.is-on .sigil { filter: drop-shadow(0 0 6px var(--color-red-glow)); }

.reaction:disabled { cursor: default; }

.sigil {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  /* Sparks and smoke are drawn out near the edges of the viewBox. */
  overflow: visible;
  transform-origin: 50% 50%;
}

/* ---- label ----
   Collapsed on every surface by default; unfurls on hover or keyboard
   focus. The glyphs carry the meaning, and two spelled-out pills per item
   outweighed the item itself everywhere, not only in the sidebar.

   max-width rather than display:none, for two reasons: it can animate,
   and it stays in the accessibility tree. There is no title attribute on
   these buttons, so this label IS the button's accessible name — nothing
   here may ever remove it from the tree. The negative margin swallows the
   flex gap while collapsed, so the chip closes up completely. */
.reaction__label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: lowercase;
  white-space: nowrap;
  max-width: 0;
  opacity: 0;
  overflow: hidden;
  margin-right: -0.45rem;
  transition: max-width var(--transition-base),
              opacity var(--transition-fast),
              margin-right var(--transition-base);
}

.reaction:hover .reaction__label,
.reaction:focus-visible .reaction__label {
  max-width: 7rem;
  opacity: 1;
  margin-right: 0;
}

/* A step above the label on purpose. The number is the actual data here,
   and 0.7rem is already on the site's scale (the toast and section
   subtitles use it), so this is not a new size. */
.reaction__count {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--color-date);
  min-width: 1ch;
  font-variant-numeric: tabular-nums;
}

.reaction.is-on .reaction__count { color: var(--color-red-bright); }

/* ---- motion ----
   Two layers combined: the glyph reacts (beat / rattle) and decoration
   appears (sparks / smoke). Both play on hover, and once as a burst when
   a vote is cast.

   Nothing loops at rest. A thoughts panel with six entries all sparkling
   continuously would be unbearable, and this site's house style is to
   breathe rather than perform. */

@keyframes sigil-beat {
  0%, 100% { transform: scale(1); }
  14%      { transform: scale(1.16); }
  28%      { transform: scale(1); }
  42%      { transform: scale(1.1); }
  56%      { transform: scale(1); }
}

@keyframes sigil-rattle {
  0%, 100% { transform: rotate(0deg); }
  20%      { transform: rotate(-7deg); }
  40%      { transform: rotate(6deg); }
  60%      { transform: rotate(-4deg); }
  80%      { transform: rotate(3deg); }
}

@keyframes sigil-spark {
  0%   { opacity: 0; transform: translateY(2px) scale(0.2); }
  30%  { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-4px) scale(0.25); }
}

@keyframes sigil-smoke {
  0%   { opacity: 0; transform: translateY(2px) scaleX(0.8); }
  25%  { opacity: 0.85; }
  100% { opacity: 0; transform: translateY(-6px) scaleX(1.25); }
}

/* Invisible unless something is animating them. fill-box so scale and
   rotate pivot on each mark rather than on the SVG's origin corner. */
.sigil__fx {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}

/* Staggered by explicit classes, not :nth-of-type — the two glyphs have
   different numbers of <path> siblings ahead of their decoration, so a
   positional selector would land on the wrong marks. */
.sigil__fx--b { animation-delay: 0.35s !important; }
.sigil__fx--c { animation-delay: 0.7s !important; }

.reaction:hover .sigil__spark { animation: sigil-spark 1.5s ease-out infinite; }
.reaction:hover .sigil__smoke { animation: sigil-smoke 2.2s ease-out infinite; }
.reaction[data-vote="1"]:hover .sigil  { animation: sigil-beat 1.6s ease-in-out infinite; }
.reaction[data-vote="-1"]:hover .sigil { animation: sigil-rattle 0.6s ease-in-out infinite; }

/* Cast: one shot, sharper than the hover loop. js/reactions.js strips
   .is-casting once it has played so hover can take over again. */
.reaction.is-casting .sigil__spark { animation: sigil-spark 700ms ease-out; }
.reaction.is-casting .sigil__smoke { animation: sigil-smoke 900ms ease-out; }
.reaction.is-casting[data-vote="1"] .sigil  { animation: sigil-beat 620ms ease-out; }
.reaction.is-casting[data-vote="-1"] .sigil { animation: sigil-rattle 520ms ease-out; }

/* Write rejected. A short shake says "that didn't save" without inventing
   chrome, and without a red flash — which on a glyph that turns red when
   it IS your vote would read as success. No toast, same restraint as the
   shoutbox's inline "failed" button state.

   Now that no surface has a pill this replaces the red edge everywhere;
   the edge survives only as the reduced-motion fallback further down. */
@keyframes reaction-reject {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-3px); }
  40%      { transform: translateX(3px); }
  60%      { transform: translateX(-2px); }
  80%      { transform: translateX(2px); }
}

.reactions.is-failed { animation: reaction-reject 320ms ease; }

/* ---- compact variant ----
   Now that nothing has a pill, this differs from the base only in scale:
   a smaller glyph and tighter spacing, for the homepage sidebar where the
   bars sit beside short lines rather than under an article. */
.reactions--compact { gap: 0.5rem; }

.reactions--compact .reaction {
  padding: 0.15rem 0.2rem;
  gap: 0.3rem;
}

.reactions--compact .sigil { width: 18px; height: 18px; }
.reactions--compact .reaction__label { margin-right: -0.3rem; }

/* ---- pointers and screens ----
   Touch has no hover, so a label that only appears on hover is a label
   nobody on a phone ever reads. Show it outright once the pointer is
   coarse — except on genuinely narrow screens, handled below. */
@media (pointer: coarse) {
  .reaction__label {
    max-width: 7rem;
    opacity: 1;
    margin-right: 0;
  }
  /* The pill used to supply a comfortable tap target. With it gone the
     padding has to, even though nothing is drawn there. */
  .reaction { min-height: 40px; }
}

/* Narrow screens keep the sigils and counts only.

   Clipped rather than display:none. The label is the button's accessible
   name, and display:none would take it out of the accessibility tree
   entirely, leaving a nameless button for anyone using a screen reader.
   This hides it from sight while keeping it announced. Placed after the
   coarse-pointer block on purpose, so a phone lands here and a tablet
   keeps the readable label. */
@media (max-width: 480px) {
  .reaction__label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    max-width: none;
    opacity: 1;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .reaction { padding: 0.4rem 0.6rem; }
}

@media (prefers-reduced-motion: reduce) {
  .reaction .sigil,
  .reaction .sigil__fx,
  .reactions.is-failed {
    animation: none !important;
  }
  /* No motion available, so the rejected write falls back to the edge
     after all — legibility beats elegance when the alternative is silent
     failure. This is the only thing the transparent border box is for. */
  .reactions.is-failed .reaction { border-color: var(--color-red-bright); }
}

/* --- Per-surface placement ---
   The blog post's bar is positioned by .post__actions and the shorts
   card's by .short-actions, both of which are flex rows that own their
   own spacing. Only the thoughts feed, where the bar stands alone under
   the card, needs a margin here — hence the :not(). Without it this rule
   wins on load order (reactions.css comes after feed.css at equal
   specificity) and pushes the shorts bar out of alignment. */

.feed-entry:not(.feed-entry--short) .reactions { margin: 0.7rem 0 0; }
