/*
  variable-vis.css
  ------------------------------------------------------------------
  Encode a signed continuous variable (e.g. a sentiment/quality score
  from -1 to 1) on any inline or block element, subtly enough to read
  through without breaking flow.

  USAGE
  ------------------------------------------------------------------
  1. Add the base class `tvv` to any div/span.
  2. On a shared ancestor (container, <body>, whatever wraps the
     whole dataset), set `--tvv-scale` to the largest magnitude your
     dataset's raw values actually reach. This lets the same markup
     work whether your scores run -1..1, -100..100, or anything else
     - see SCALE INVARIANCE below. Default is 1 (i.e. values already
     normalized to -1..1).
  3. Set the value as an inline custom property, in your data's own
     units:

       <div class="tvv tvv-edge tvv-glow" style="--tvv-value: -0.6">
         I have measured out my life with coffee spoons.
       </div>

  4. Optionally set `--tvv-confidence` (0..1, default 1) to fade the
     whole encoding when a score is uncertain.

  5. Pick ONE fill method, ONE edge method, ONE margin method (each
     group occupies a single CSS slot - see COMPOSABILITY below) and
     as many free-standing modifiers (tvv-glow, tvv-ink, tvv-weight,
     tvv-shift, tvv-radius) as you like.

  COMPOSABILITY
  ------------------------------------------------------------------
  A single element only has one `background`, one `::before` and one
  `::after`. So the modifiers below are grouped into three "slots":

    fill slot    (background)   tvv-wash | tvv-texture | tvv-corner
    edge slot    (::after)      tvv-edge | tvv-center
    margin slot  (::before)     tvv-gutter | tvv-dot

  Pick at most one class from each slot. The rest of the modifiers
  (tvv-glow, tvv-ink, tvv-weight, tvv-shift, tvv-radius) use
  box-shadow / color / font / transform / border-radius respectively,
  so they're free to combine with anything, including each other.

  SCALE INVARIANCE
  ------------------------------------------------------------------
  CSS has no way to inspect "what's the largest value among all my
  sibling elements" - it can only see one element's own declared
  custom properties. So normalization against the dataset's actual
  range has to be supplied explicitly, via --tvv-scale, rather than
  inferred automatically:

       <body style="--tvv-scale: 100">
         <div class="tvv tvv-edge" style="--tvv-value: -60">...</div>
         <div class="tvv tvv-edge" style="--tvv-value: 90">...</div>
       </body>

  Every visual effect is computed from --tvv-value / --tvv-scale, so
  scaling an entire dataset by a constant k and setting --tvv-scale to
  (old-scale * k) reproduces an IDENTICAL picture - the visualization
  depends only on each value's position relative to the dataset's own
  scale, not on the absolute numbers. That's different from clamping:
  clamping just saturates outliers within one fixed assumed range
  (which is what happens if you forget to set --tvv-scale and feed it
  raw values from a -100..100 dataset - everything pegs to full
  magnitude and the variation within the dataset is lost).

  If you don't know your dataset's max magnitude ahead of time, the
  simplest fix is one line of JS that scans your data array for
  Math.max(...values.map(Math.abs)) and sets it as --tvv-scale on the
  container before rendering - CSS can't do that scan itself.

  ACCESSIBILITY
  ------------------------------------------------------------------
  Color-coded methods (tvv-wash, tvv-gutter, tvv-corner, tvv-glow,
  tvv-ink) are invisible to some color-vision-deficient readers.
  Pair at least one position/shape-based method (tvv-edge, tvv-center,
  tvv-shift, tvv-dot, tvv-radius) with any color method so meaning
  survives even if hue doesn't.

  REVEAL MODES
  ------------------------------------------------------------------
  Wrap the text in a container and toggle a mode class to globally
  scale intensity without touching individual lines:

       .tvv-mode-off       everything invisible
       .tvv-mode-subtle    default, peripheral
       .tvv-mode-explicit  legible at a glance (for analysis/editing)

  Or rely on the built-in :hover boost for an on-demand reveal.
*/

/* ----------------------------------------------------------------
   0. Base: derive sign/magnitude from the single signed input.
      No abs() dependency - works in any browser that supports
      calc() and custom properties (i.e. all current browsers).
   ---------------------------------------------------------------- */

/* --tvv-scale is the "value that should read as full magnitude" for a
   dataset. It defaults to 1 at the root and is meant to be overridden
   on a shared ancestor (container, <body>, whatever wraps the whole
   dataset) so every .tvv element inherits the same scale - it must
   NOT be declared inside the .tvv rule itself, since a value declared
   directly on an element always wins over inheritance, which would
   silently defeat any ancestor override. If a dataset's raw values
   run -100..100, set --tvv-scale: 100 on the container; if it's
   fractional sentiment scores running -0.01..0.01, set --tvv-scale:
   0.01. Multiplying every value AND --tvv-scale by the same factor
   reproduces an identical picture, which is the actual goal: the
   visualization should be invariant to the dataset's unit choice, not
   just clamp outliers within one fixed assumed unit. */
:root {
  --tvv-scale: 1;
}

.tvv {
  --tvv-value: 0;            /* signed input, in whatever units your data uses */
  --tvv-confidence: 1;       /* 0..1, optional, set inline per element */

  --tvv-norm: calc(var(--tvv-value) / var(--tvv-scale));

  --tvv-pos: clamp(0, var(--tvv-norm), 1);
  --tvv-neg: clamp(0, calc(var(--tvv-norm) * -1), 1);
  --tvv-mag: calc(var(--tvv-pos) + var(--tvv-neg)); /* 0..1, sign-agnostic */

  /* Color tokens: low saturation, high lightness so washes read as a
     faint cast rather than a highlight. Override per-theme as needed. */
  --tvv-hue-neg: 18;   /* warm */
  --tvv-hue-pos: 215;  /* cool */
  --tvv-sat: 32%;
  --tvv-light: 86%;
  --tvv-color-neg: hsl(var(--tvv-hue-neg) var(--tvv-sat) var(--tvv-light));
  --tvv-color-pos: hsl(var(--tvv-hue-pos) var(--tvv-sat) var(--tvv-light));

  /* Global intensity multiplier, overridden by reveal-mode ancestors
     and boosted slightly on hover for an on-demand reveal. */
  --tvv-intensity: calc(1 * var(--tvv-confidence));

  position: relative;
}

.tvv:hover {
  --tvv-intensity: calc(1.6 * var(--tvv-confidence));
}

/* Reveal modes — apply to an ancestor (e.g. <body class="tvv-mode-subtle">) */
.tvv-mode-off    .tvv { --tvv-intensity: 0; }
.tvv-mode-subtle .tvv { --tvv-intensity: calc(1 * var(--tvv-confidence)); }
.tvv-mode-explicit .tvv { --tvv-intensity: calc(2.4 * var(--tvv-confidence)); }

/* A neutral color-mix shared by several modifiers below: blends the
   negative and positive tokens by their respective weights. Since at
   most one of --tvv-pos/--tvv-neg is nonzero for a given value, this
   always resolves to "the" color for that sign. */
.tvv {
  --tvv-color: color-mix(
    in srgb,
    var(--tvv-color-neg) calc(var(--tvv-pos) * 0% + var(--tvv-neg) * 100%),
    var(--tvv-color-pos) calc(var(--tvv-pos) * 100% + var(--tvv-neg) * 0%)
  );

  /* Sign as a clean 0/1 switch (not magnitude-weighted) for modifiers
     that need to pick between two fixed positions rather than
     interpolate a continuous offset. At value 0 both are 0, which is
     fine since magnitude-driven opacity/size will be 0 too. */
  --tvv-neg-bool: calc(var(--tvv-neg) / max(var(--tvv-mag), 0.0001));
  --tvv-pos-bool: calc(var(--tvv-pos) / max(var(--tvv-mag), 0.0001));
}

/* ----------------------------------------------------------------
   1. FILL SLOT (background) — pick at most one.
   ---------------------------------------------------------------- */

/* 1a. Wash — Claude's background-color idea, ChatGPT/Gemini agree
   this is the safest baseline for long reading: the eye adapts and
   stops noticing it. */
.tvv-wash {
  background-color: hsl(
    from var(--tvv-color) h s l / calc(var(--tvv-mag) * 0.5 * var(--tvv-intensity))
  );
}

/* 1b. Texture — ChatGPT's idea that humans detect density changes
   without consciously registering them. Stripe angle doubles as a
   sign cue for colorblind-safe redundancy even within the fill slot:
   vertical (90deg) at zero, tilting toward "/" (lower angle) for
   positive and "\" (higher angle) for negative. */
.tvv-texture {
  --tvv-texture-tilt: 25deg;
  background-image: repeating-linear-gradient(
    calc(90deg + var(--tvv-pos) * var(--tvv-texture-tilt) - var(--tvv-neg) * var(--tvv-texture-tilt)),
    hsl(from var(--tvv-color) h s l / calc(var(--tvv-mag) * 0.35 * var(--tvv-intensity))) 0 1px,
    transparent 1px 6px
  );
}

/* 1c. Corner marker — ChatGPT's idea, implemented as a radial-gradient
   background layer (rather than a pseudo-element) so it stays in the
   same slot as the other fill methods. Bottom-left for negative,
   bottom-right for positive. */
.tvv-corner {
  background-image: radial-gradient(
    circle at calc(50% - var(--tvv-neg) * 50% + var(--tvv-pos) * 50%) 100%,
    hsl(from var(--tvv-color) h s l / calc(var(--tvv-mag) * 0.45 * var(--tvv-intensity))) 0%,
    transparent calc(8% + var(--tvv-mag) * 10%)
  );
}

/* ----------------------------------------------------------------
   2. EDGE SLOT (::after) — pick at most one.
   ---------------------------------------------------------------- */

/* 2a. Edge underline — the user-validated design from the ChatGPT
   chat: a thin bar that grows inward from the left margin (negative)
   or right margin (positive). Implemented as a single gradient so it
   never needs a sign-dependent `left`/`right` switch. */
.tvv-edge {
  --tvv-edge-height: 4px;
  --tvv-edge-max: 45%; /* cap so extreme values don't dominate */
}
.tvv-edge::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--tvv-edge-height);
  border-radius: 999px;
  opacity: calc(var(--tvv-intensity));
  background: linear-gradient(
    to right,
    var(--tvv-color-neg) 0%,
    var(--tvv-color-neg) calc(var(--tvv-neg) * var(--tvv-edge-max)),
    transparent calc(var(--tvv-neg) * var(--tvv-edge-max)),
    transparent calc(100% - var(--tvv-pos) * var(--tvv-edge-max)),
    var(--tvv-color-pos) calc(100% - var(--tvv-pos) * var(--tvv-edge-max)),
    var(--tvv-color-pos) 100%
  );
}

/* 2b. Center-out microbar — Gemini's "adaptive underline" / ChatGPT's
   "center-out microbar": instead of growing from the margins, it
   grows from the midline, which reads as a literal zero-axis once
   the reader has seen a few lines. */
.tvv-center {
  --tvv-center-height: 3px;
  --tvv-center-max: 22%;
}
.tvv-center::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 50%;
  height: var(--tvv-center-height);
  border-radius: 999px;
  opacity: var(--tvv-intensity);
  background: var(--tvv-color);
  width: calc(var(--tvv-mag) * var(--tvv-center-max));
  /* translateX% is relative to the bar's OWN width, which varies by
     magnitude - using --tvv-neg/--tvv-pos (magnitude-weighted) here
     instead of the boolean switch made partial-magnitude bars shift
     only partway, so the anchor drifted off-center line to line.
     --tvv-neg-bool is a clean 0/1, so the shift is always exactly
     0% or -100% of self regardless of how wide the bar is. */
  transform: translateX(calc(-100% * var(--tvv-neg-bool)));
  /* negative: bar's right edge sits at center, growing left;
     positive: bar's left edge sits at center, growing right */
}

/* ----------------------------------------------------------------
   3. MARGIN SLOT (::before) — pick at most one. Requires the parent
      list/container to leave room outside the text column (e.g.
      padding-left/right or a wide max-width margin).
   ---------------------------------------------------------------- */

/* 3a. Gutter stripe — Claude's left-border accent + Gemini's gutter
   indicator + ChatGPT's margin glyphs, merged: a fixed-size stripe
   whose side is sign and whose opacity is magnitude. Claude's
   refinement (fixed width, variable opacity rather than variable
   width) avoids the layout-rhythm disruption of a growing bar.

   Both signs sit in the LEFT margin, in two lanes (near = negative,
   far = positive), rather than one lane left and one right. With
   poetry, the right edge of a line of text rarely matches the right
   edge of the containing element, so a right-anchored mark reads as
   misaligned; anchoring both lanes to the left keeps it legible.
   (Also: an absolutely positioned element with `width` set ignores
   `right` per spec, so left+right+width together silently collapses
   every sign to the same spot — this rewrite avoids that trap by
   only ever setting `left`.) */
/* Shared by both margin-slot methods so they read as the same
   "family" of mark rather than two unrelated colors. Pitched halfway
   between the very faint fill/edge wash (32% sat / 86% light) and a
   fully saturated accent — bold enough to read in dead margin space
   without becoming a distracting graphic element. */
.tvv-gutter, .tvv-dot {
  --tvv-margin-color-neg: hsl(var(--tvv-hue-neg) 46% 64%);
  --tvv-margin-color-pos: hsl(var(--tvv-hue-pos) 46% 64%);
  --tvv-margin-color: color-mix(
    in srgb,
    var(--tvv-margin-color-neg) calc(var(--tvv-neg-bool) * 100%),
    var(--tvv-margin-color-pos) calc(var(--tvv-pos-bool) * 100%)
  );
}

.tvv-gutter {
  --tvv-gutter-lane-near: 14px;
  --tvv-gutter-lane-far: 24px;
  --tvv-gutter-width: 3px;
}
.tvv-gutter::before {
  content: "";
  position: absolute;
  top: 0.15em;
  bottom: 0.15em;
  width: var(--tvv-gutter-width);
  border-radius: 999px;
  background: var(--tvv-margin-color);
  opacity: calc(var(--tvv-mag) * 0.8 * var(--tvv-intensity));
  left: calc(-1 * (
    var(--tvv-gutter-lane-near) * var(--tvv-neg-bool) +
    var(--tvv-gutter-lane-far) * var(--tvv-pos-bool)
  ));
}

/* 3b. Margin dot — same lane logic and color as the gutter, sized by
   magnitude rather than opacity. */
.tvv-dot {
  --tvv-dot-lane-near: 16px;
  --tvv-dot-lane-far: 28px;
  --tvv-dot-base: 4px;
  --tvv-dot-max: 9px;
}
.tvv-dot::before {
  content: "";
  position: absolute;
  top: 50%;
  width: calc(var(--tvv-dot-base) + var(--tvv-mag) * var(--tvv-dot-max));
  height: calc(var(--tvv-dot-base) + var(--tvv-mag) * var(--tvv-dot-max));
  border-radius: 50%;
  background: var(--tvv-margin-color);
  /* min(mag*1000, 1) is a hard 0 at mag=0 (true neutral, invisible)
     and ~1 for any nonzero mag, so the 0.5 floor below boosts
     visibility of small-but-real values without ever drawing a dot
     for an exact-zero score. */
  opacity: calc((0.5 + var(--tvv-mag) * 0.5) * var(--tvv-intensity) * min(var(--tvv-mag) * 1000, 1));
  transform: translateY(-50%);
  left: calc(-1 * (
    var(--tvv-dot-lane-near) * var(--tvv-neg-bool) +
    var(--tvv-dot-lane-far) * var(--tvv-pos-bool)
  ));
}

/* ----------------------------------------------------------------
   4. FREE MODIFIERS — composable with anything and each other,
      since each uses a property no other modifier touches.
   ---------------------------------------------------------------- */

/* 4a. Inset side glow — identical idea independently proposed by
   Claude and ChatGPT: an ambient bleed rather than a hard edge.
   Direction flips with sign via a signed offset. */
.tvv-glow {
  --tvv-glow-x: calc((var(--tvv-pos) - var(--tvv-neg)) * 16px);
  box-shadow: inset var(--tvv-glow-x) 0 22px 4px
    hsl(from var(--tvv-color) h s l / calc((0.2 + var(--tvv-mag) * 0.4) * var(--tvv-intensity) * min(var(--tvv-mag) * 1000, 1)));
}

/* 4b. Ink nudge — Gemini's opacity/contrast fade, combined with
   Claude's "text hue nudge" caveat (use a tiny hue shift, not a
   visible color change, and don't touch saturation/size/line-height).
   Negative dims slightly toward the page background; positive stays
   fully crisp. */
.tvv-ink {
  opacity: calc(1 - var(--tvv-neg) * 0.3 * min(var(--tvv-intensity), 1));
  /* l is a unitless number (0-100) in relative-color syntax, not an
     angle - the nudge here is "+6 lightness at full positive". */
  color: hsl(from currentColor h s calc(l + var(--tvv-pos) * 6 * min(var(--tvv-intensity), 1)));
}

/* 4c. Variable font weight/width — Gemini's typographic-axis idea.
   With a real variable font (one with a `wght` axis) this is a
   smooth continuous nudge across the whole +/-1 range. Most static
   fonts only ship Regular(400)/Bold(700) as separate files, so any
   in-between calc() result just snaps to Regular and looks like a
   no-op - there is no intermediate face to render. The asymmetric
   range below (positive reaches all the way to 700) means even a
   static font shows *something* at strong positive values, as a
   degraded fallback; full fidelity still requires a variable font. */
.tvv-weight {
  --tvv-weight-val: calc(400 + var(--tvv-pos) * 300 * min(var(--tvv-intensity), 1) - var(--tvv-neg) * 50 * min(var(--tvv-intensity), 1));
  font-weight: var(--tvv-weight-val);
  font-variation-settings: "wght" var(--tvv-weight-val);
}

/* 4d. Horizontal shift — ChatGPT's "wave" pattern: positionally
   encodes sign/magnitude, which is the most colorblind-robust method
   here since it needs no color at all. Keep displacement small. */
.tvv-shift {
  transform: translateX(calc((var(--tvv-pos) - var(--tvv-neg)) * 5px * min(var(--tvv-intensity), 1)));
}

/* 4e. Asymmetric corner radius — ChatGPT's border-curvature idea:
   invisible on one line, legible as a rhythm across many. */
.tvv-radius {
  --tvv-radius-max: 10px;
  border-radius:
    calc(var(--tvv-neg) * var(--tvv-radius-max)) calc(var(--tvv-pos) * var(--tvv-radius-max))
    calc(var(--tvv-pos) * var(--tvv-radius-max)) calc(var(--tvv-neg) * var(--tvv-radius-max));
}

/* ----------------------------------------------------------------
   5. Respect reduced-motion / reduced-transparency preferences.
   ---------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .tvv, .tvv::before, .tvv::after {
    transition: none !important;
  }
}
