/* ---------------------------------------------------------
   Whimsy axis — the structural parts a token cannot express.
   Middle needs none of it; Softish and Harsh each have one
   thing that is a property rather than a value.
   --------------------------------------------------------- */

/* ---- 0: softish - a photograph becomes a print ---------------------------
   Mat drawn inside the item, insetting the picture. The card is made taller
   than its photo to hold it instead (canvas/picture-shape.ts), so the window
   is the photo's own shape and nothing is cropped - which is why crossing into
   Softish reshapes the cards. Every measure is a fraction of --half-min so the
   mat scales with the print; PRINT_MAT and PRINT_CHIN in geometry.ts are these
   numbers, and must move with them. */
/* :not([data-bare]): a cut-out has no card, so the print treatment does not
   apply — it would draw a frame round nothing. */
:root[data-whimsy="0"] .item[data-type="image"]:not([data-bare]) {
  --mat:  calc(var(--half-min, 120px) * 0.08);
  --chin: calc(var(--half-min, 120px) * 0.26);
  /* Fraction of --radius so the Corner slider still moves it. */
  --shape-radius: calc(var(--radius) * 0.12);
}
/* Absolute + explicit size, not inset offsets — a replaced element with auto
   width resolves to intrinsic size, not the gap between offsets. */
:root[data-whimsy="0"] .item[data-type="image"]:not([data-bare]) .item-body img {
  position: absolute;
  left: var(--mat);
  top:  var(--mat);
  width:  calc(100% - var(--mat) * 2);
  height: calc(100% - var(--mat) - var(--chin));
  border-radius: 0;
  /* Hairline for pale photos on pale stock. Not round a letterboxed photo,
     where it would outline the empty window rather than the picture. */
  box-shadow: 0 0 0 var(--board-hairline) color-mix(in srgb, var(--ink) 10%, transparent);
}
:root[data-whimsy="0"] .item[data-type="image"][data-fit="contain"]:not([data-bare]) .item-body img {
  box-shadow: none;
}
/* Caption sits on the chin — no translucent bar needed. Stays visible
   (opacity: 1) where other tiers hide it until hover. */
:root[data-whimsy="0"] .item[data-type="image"] .item-bar {
  background: none;
  border-top: 0;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  opacity: 1;
}
:root[data-whimsy="0"] .item[data-type="image"] .item-label {
  height: var(--chin);
  line-height: var(--chin);
  padding: 0 var(--mat);
  text-align: center;
  font-size: clamp(11px, calc(var(--half-min, 120px) * 0.09), 20px);
}

/* Round, not nearly round. --leaf is 20px at this end, which caps a 35px
   control to a pill but left the 42-45px glass buttons and the two short
   drawer buttons a pixel or two short of one: a circle drawn slightly wrong.
   These go the rest of the way. A new glass control joins this list. */
:root[data-whimsy="0"] :is(#menu-btn, #mobile-header-edit-btn, #mobile-find-btn,
  #drawer-btn, #history-ctl, #zoom-ctl, #toolbar, #toolbar-tools,
  .field .pigment-swatch, .drawer-special, .drawer-empty) {
  border-radius: var(--radius-pill);
}

/* Origin mark shape per tier: Softish keeps the compass ring, Harsh squares
   it to a registration mark. Middle is the unset default (rounded square). */
#origin-mark .om-ring,
#origin-mark .om-ring-sq,
#origin-mark .om-pip-sq { display: none; }
:root[data-whimsy="0"] #origin-mark .om-ring-rd { display: none; }
:root[data-whimsy="0"] #origin-mark .om-ring { display: inline; }
:root[data-whimsy="2"] #origin-mark .om-ring-rd,
:root[data-whimsy="2"] #origin-mark .om-pip { display: none; }
:root[data-whimsy="2"] #origin-mark .om-ring-sq,
:root[data-whimsy="2"] #origin-mark .om-pip-sq { display: inline; }

/* No inner rule at Harsh. Unseen rather than `content: none`, so the rule is
   still there to ease its corner when the tier changes (tokens.css, 1c): one
   that stops existing appears at its end radius inside a card still easing.
   A fence's ::after is its grain, not a rule, and still goes altogether. */
:root[data-whimsy="2"] .card::after { border-color: transparent; }
:root[data-whimsy="2"] .fence-card::after { content: none; }

/* No lean at Harsh: a 1px black rectangle turned 1.5deg has no crisp edge.
   `rotate` outright because tilt tokens can be set inline on :root. */
:root[data-whimsy="2"] .item { rotate: 0deg; }

/* Stickies go flat: ruled edge, no adhesive band. --note-sheet is a faded
   note's paper (cards.css), which this rule outranks and so has to read. */
:root[data-whimsy="2"] .item[data-type="note"] .card {
  background: var(--note-sheet, var(--note-paper));
  box-shadow: inset 0 0 0 var(--board-hairline) var(--ink);
}

/* Lozenges become squares. */
:root[data-whimsy="2"] .grip,
:root[data-whimsy="2"] :is(.field, .np-volume, .ctx-range, .dk-dial, .xo-dial) input[type="range"]::-webkit-slider-thumb { transform: none; }

/* Toast sits straight. */
:root[data-whimsy="2"] .toast-line {
  transform: none;
  animation-name: rise-plain;
}
@keyframes rise-plain { from { opacity: 0; transform: translateY(6px); } }

/* Now-playing entrance per tier. Softish throws it (further under, smaller,
   off-square before settling); Harsh slides only (no overshoot, no scale). */
:root[data-whimsy="0"] #nowplaying {
  --np-under: calc(100% + 44px);
  --np-shrink: 0.93;
  --np-tilt: -1.1deg;
}
:root[data-whimsy="2"] #nowplaying {
  --np-shrink: 1;
  --np-tilt: 0deg;
  --np-ease: var(--ease);
}

/* Close button stops spinning at Harsh. */
:root[data-whimsy="2"] #side-close:hover,
:root[data-whimsy="2"] #header-close:hover { transform: none; }

/* Hamburger wave: three bars sample one sine at 0/120/240 degrees, Softish
   only. Eight keyframe samples at 45-degree steps with linear timing (error
   ~0.1px). Positive delays so the wave builds over its first lap. Behind
   (hover: hover) because a latched :hover on touch is worse than no wave. */
@media (hover: hover) {
  :root[data-whimsy="0"] #menu-btn:hover span {
    animation: menu-wave calc(var(--dur-base) * 5.95) linear infinite;
  }
  :root[data-whimsy="0"] #menu-btn:hover span:nth-child(2) {
    animation-delay: calc(var(--dur-base) * 1.98333);
  }
  :root[data-whimsy="0"] #menu-btn:hover span:nth-child(3) {
    animation-delay: calc(var(--dur-base) * 3.96667);
  }
}
/* sin(45deg) * 3px = 2.121px; symmetric about crest and zero. */
@keyframes menu-wave {
  0%, 50%, 100% { transform: none; }
  12.5%, 37.5%  { transform: translateX(2.121px); }
  25%           { transform: translateX(3px); }
  62.5%, 87.5%  { transform: translateX(-2.121px); }
  75%           { transform: translateX(-3px); }
}

/* Whimsy stop labels: each name is a specimen of its tier, so faces and
   weights are pinned (never --font-display or --display-weight, which are
   what the slider moves). Both the panel row and the ghost card row. */
.field-stops {
  display: flex;
  justify-content: space-between;
  margin-top: 3px;
  color: var(--ink-3);
  font-size: var(--t-tiny);
}
#whimsy-stop-labels span:nth-child(1),
.whimsy-stops span:nth-child(1) {
  font-family: var(--font-serif-text);
  font-style: italic;
  font-weight: 400;
}
#whimsy-stop-labels span:nth-child(2),
.whimsy-stops span:nth-child(2) {
  color: var(--ink-2);
  font-family: var(--font-sans);
  font-style: italic;
  font-weight: 400;
}
#whimsy-stop-labels span:nth-child(3),
.whimsy-stops span:nth-child(3) {
  color: var(--ink);
  font-family: var(--font-sans);
  font-style: normal;
  font-weight: 700;
}

/* ---------------------------------------------------------------------------
   Quality: the two things that are not tokens

   Backdrop-filter and animation/transition durations are properties, not
   custom properties, so they must be cancelled where set — universal selector
   with !important, same shape as prefers-reduced-motion.
   --------------------------------------------------------------------------- */

/* Backdrop blur: re-sampled every frame, most expensive per pixel, least missed. */
:root[data-quality="light"]:not([data-q-blur]) *,
:root[data-q-blur="off"] * {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}

/* Paper grain: a window-sized layer that multiply-blends with the page behind
   it, repositioned on every pan frame and re-scaled - so resampled - on every
   zoom frame (canvas/grain.ts). A blended layer cannot be composited past its
   backdrop, which makes that a full-viewport repaint per frame for a texture
   nobody looks at while the board is moving. `display: none` rather than
   `opacity: 0`: a transparent layer is still painted and still blended.

   #grain only, not the world-space grain on a fence or the Mobile sheet: those
   ride inside the board's own transform, are composited once and cost nothing
   per frame, so taking them away would be fidelity given up for no speed. */
:root[data-quality="light"]:not([data-q-grain]) #grain,
:root[data-q-grain="off"] #grain { display: none; }

/* Animations: catches hand-written durations the token flatten misses.
   One lap as well as a hundredth of a millisecond: an `infinite` loop cut in
   duration alone runs a hundred thousand laps a second, so every frame lands
   at a random point in it - a strobe, and a frame asked for on every vsync for
   as long as it is on screen. */
:root[data-quality="light"]:not([data-q-anim]) *,
:root[data-q-anim="off"] * {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
}

/* And the canvas is exempt from that last line, at 0s rather than a hundredth
   of a millisecond. The difference is not the length: it is whether a
   transition runs at all.

   `transition-property` is `all` until something says otherwise, and almost
   nothing under #viewport does. A duration of 0.01ms therefore does not shorten
   those elements' transitions - it gives them transitions they never had, on
   every property, and the canvas is the one part of the app whose properties are
   rewritten on every frame of a pan or a zoom. #world's transform, --iz and
   every length derived from it, a card's left and top: each write became a
   fresh compositor animation, sixty times a second, and Chrome on a phone
   answered by re-rasterising the board over and over - which reads as every
   card on screen unloading and loading again, at Light and at no other stop.

   0s is the state those elements were in before this block: a transition whose
   duration and delay sum to zero does not run and fires no event. Nothing under
   #viewport waits on `transitionend` (the exits there are animations, which are
   untouched above), so there is no event to lose - which is exactly why the
   chrome outside it keeps the 0.01ms, where a lost `transitionend` would strand
   a panel mid-close. */
:root[data-quality="light"]:not([data-q-anim]) :is(#viewport, #viewport *),
:root[data-q-anim="off"] :is(#viewport, #viewport *) {
  transition-duration: 0s !important;
}

/* Where one lap ends on the wrong frame to hold, the answer the
   prefers-reduced-motion block beside each animation already gives. Repeated
   here because the dial cannot reach inside a media query. The splash is the
   one that matters most: index.html sets the level before first paint, and a
   splash card's last frame is invisible. */
:root[data-quality="light"]:not([data-q-anim]) :is(.splash-card, .splash-line),
:root[data-q-anim="off"] :is(.splash-card, .splash-line) { animation: none; }
:root[data-quality="light"]:not([data-q-anim]) .splash-card,
:root[data-q-anim="off"] .splash-card { transform: rotate(var(--splash-tilt, 0deg)); }
/* A stopped indeterminate bar reads as progress stuck at 38%. */
:root[data-quality="light"]:not([data-q-anim]) #busy:not(.is-counting) #busy-fill,
:root[data-q-anim="off"] #busy:not(.is-counting) #busy-fill {
  width: 100%;
  opacity: 0.4;
  animation: none;
}
:root[data-quality="light"]:not([data-q-anim]) #darkroom.dk-busy #darkroom-plate,
:root[data-q-anim="off"] #darkroom.dk-busy #darkroom-plate { animation: none; opacity: 0.62; }
:root[data-quality="light"]:not([data-q-anim]) .pl-eq i,
:root[data-q-anim="off"] .pl-eq i { animation: none; transform: scaleY(0.7); }

/* ---------------------------------------------------------------------------
   System preferences

   prefers-reduced-transparency: a different question from the quality blur
   stop, with the same answer. Here (not base.css) because the fix is the
   same universal+!important shape as the quality block above.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-transparency: reduce) {
  * {
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }
}

/* prefers-contrast: more — scrims from 42% to 78%. Not opaque, so the board
   behind is still communicated. color-mix() only — below WebKit 16.2 the
   42% scrim stands (less contrast, still working). */
@media (prefers-contrast: more) {
  #ask::backdrop,
  #credits::backdrop,
  #inventory::backdrop,
  #compose::backdrop,
  #darkroom::backdrop,
  #pick::backdrop,
  #viewer::backdrop {
    background: color-mix(in srgb, var(--ink) 78%, transparent);
  }
}

/* Image/video cards have no .card, so no hairline of their own. When shadow
   is removed (Harsh or quality dial), an inset ring on ::after replaces it.
   ::after paints over .item-body so the ring is not hidden under the picture.
   :not([data-bare]) — a cut-out has no card edge to trace. */
:root[data-whimsy="2"] .item[data-type="image"]:not([data-bare])::after,
:root[data-whimsy="2"] .item[data-type="video"]:not([data-bare])::after,
:root[data-quality="light"]:not([data-q-shadows]) .item[data-type="image"]:not([data-bare])::after,
:root[data-quality="light"]:not([data-q-shadows]) .item[data-type="video"]:not([data-bare])::after,
:root[data-q-shadows="off"] .item[data-type="image"]:not([data-bare])::after,
:root[data-q-shadows="off"] .item[data-type="video"]:not([data-bare])::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 var(--board-hairline) var(--item-border);
  pointer-events: none;
  z-index: 1;
}

/* color-mix() fallback for WebKit < 16.2 — see tokens.css section 7. */
@supports not (color: color-mix(in srgb, red, blue)) {
  :root[data-whimsy="0"] .item[data-type="image"]:not([data-bare]) .item-body img {
    box-shadow: 0 0 0 var(--board-hairline) rgb(var(--ink-c) / 10%);
  }
}
