/* jelly-ui: how a jellyfish and its slider panel are drawn. The field they
   swim in stays with the app -- it is a piece of that app's layout (which
   column, how tall, what it sits behind), and the module writes only its width,
   measured off the column rather than read from here (see measureField in
   jellies.js).

   The one thing an app must give the field is a stacking context to be behind:
   `isolation: isolate` on the column, so the field's z-index: -1 puts it under
   the column's text and above its background instead of escaping to the root.
   A worked example, from both consumers:

       #sidebar        { isolation: isolate; }
       #sidebar_jellies { position: fixed; top: 0px; left: 0px; height: 100vh;
                          z-index: -1; overflow: hidden; pointer-events: none; } */

/* Both halves of a jelly's motion are transforms, and an element has one
   transform property, so they get one element each: .jelly carries the position
   and the heading, .jelly_body the squeeze of the bell. Both are written per
   frame from jellies.js -- there are no keyframes here, because the squeeze is
   what drives the swimming and every part of it is on a slider.

   Nesting them this way is also what makes "flatten in length" mean anything:
   the body's scale composes *inside* the parent's rotation, so its Y is always
   along the direction of travel, whichever way the jelly is pointing. The
   shimmer's slices nest one level further in for the same reason: they slide
   the glyph across its own body, not across the screen.

   The body may also carry an inline colour filter, written once at birth -- one
   per kind, from the app's own kinds list. It's a filter rather than a colour
   because the glyph is an emoji and there is nothing here to colour; filtering
   its own colours keeps the bell's shading and pale tentacles, where a tint
   would flatten them. A kind may rotate the hue, darken it and saturate it (see
   colourFilter in jellies.js); the glyph is pale, so a deep colour needs all
   three.

   The two opacities split the same way, and multiply: the outer one is how far
   into the background the whole thing sits (and the only thing an app's dark
   mode need override -- 🪼 is a pale glyph, so the value that's a faint wash on
   white nearly disappears on black), the inner one is the glyph's own weight
   relative to it. One theme knob, one drawing knob.

   The font-size splits the same way again: the default size is on the wrapper,
   and the body may carry an inline multiple of it in `em`, written once at
   birth. That way the number itself stays here and the script only ever writes
   the spread -- and it's a real font-size rather than another scale in the
   transform, so a jelly drawn large is drawn large rather than a small one
   magnified. */
.jelly {
    position: absolute;
    top: 0px;
    left: 0px;
    opacity: 0.55;
    font-size: 30px;
    will-change: transform;
}

.jelly_body {
    display: block;
    line-height: 1;
    opacity: 0.6;

    /* positioning context for the shimmer's slices below, which stack on top of
       each other inside it. Still display: block, and the slices are blocks
       too: an inline-block anywhere in here would sit on a baseline and give
       the wrapper a line box taller than the glyph, which would move the centre
       .jelly rotates its heading around. */
    position: relative;

    /* origin up in the bell rather than the centre, so flattening gathers the
       tentacles up under it instead of squashing the whole glyph in place --
       and the snap back out is then a kick from the tentacles, which is where
       the thrust comes from on the real animal. */
    transform-origin: 50% 30%;
    will-change: transform;
}

/* THE SHIMMER -- a wave travelling down the glyph, drawn as a stack of
   horizontal slices of it that each slide sideways by a different amount.

   The stack is there because no transform can do this. Every CSS transform is
   affine, so the sideways offset it gives a pixel is always a *linear* function
   of that pixel's height -- which is a shear, and a shear of a small glyph is
   indistinguishable from turning it. That was the first pass, and it read as
   the jelly changing heading rather than swaying. A wave needs the offset to be
   a curve in y, and the only ways to bend a rendered glyph that way are to cut
   it into pieces or to run it through an SVG displacement filter; slices keep
   the motion in the frame loop with the rest of it.

   Every slice is a full copy of the glyph clipped to its own band, so they have
   to sit exactly on top of one another: the first is in flow and gives the body
   its size, the rest are absolutely positioned at the same origin. The bands
   abut rather than overlap -- an overlap would draw the glyph twice where they
   meet and show as a dark line, since the opacity that fades these into the
   background is on the body above, not on each slice.

   No will-change here: it belongs on the handful of .jelly_body elements, not
   on every slice of every one of them. */
.jelly_slice {
    display: block;
    position: absolute;
    top: 0px;
    left: 0px;
    white-space: nowrap;
}

.jelly_slice:first-child {
    position: static;
}

/* THE SIMULATION CONTROLS, pinned to the very bottom of the column.
   position: fixed like the field rather than trailing the last item in the
   column: it keeps them at the bottom of the *column* whatever the list above
   is doing, and out of the flow entirely, so nothing above them moves. The
   mount's padding-bottom reserves the strip they sit in, set from this panel's
   measured height (see reserveControlSpace) since it folds. Its width is
   written from the mount's, for the same reason the field's is.

   A <details> rather than a hand-rolled fold: this is a local panel and the
   native element already keyboard-navigates and remembers nothing we would have
   to store twice. max-height keeps a short window from handing the whole column
   over to the sliders.

   left: 0px assumes the column is the leftmost thing on the page, which it is
   in both consumers; an app whose sidebar sits elsewhere overrides this one
   declaration. */
.jelly_controls {
    position: fixed;
    bottom: 0px;
    left: 0px;
    box-sizing: border-box;
    padding: 6px 18px 10px 10px;

    max-height: 60vh;
    overflow: auto;
}

/* The panel is a development instrument rather than chrome anyone asked for, so
   it is an easter egg: it is built carrying this class, and showControls()
   takes it off for whoever finds the way in.

   display: none rather than visibility or opacity so the strip reserved for it
   at the foot of the column goes with it: reserveControlSpace() measures
   offsetHeight, which a display: none element reports as 0, so the list gets
   that space back with no second rule to keep in step. */
.jelly_controls.jelly_controls_hidden {
    display: none;
}

.jelly_controls summary {
    font-size: 9pt;
    font-weight: bold;
    text-transform: uppercase;
    color: #7D7D7D;
    cursor: pointer;
    user-select: none;
}

/* Fifteen rows in a column that also has to hold the app's own list: the label
   sits tight on its slider and the rows sit tight on each other, so the panel
   reads as one block of instrumentation rather than fifteen separate settings. */
.jelly_control {
    display: flex;
    flex-direction: column;
    margin-top: 4px;
}

.jelly_control label {
    font-size: 9pt;
    color: #7D7D7D;
}

.jelly_control input[type='range'] {
    width: 100%;
    height: 14px;
    margin: 0px;
    padding: 0px;
    border: none;
    cursor: pointer;

    /* currentColor rather than a literal, so the thumb and track follow the
       body text through a dark-mode flip with no override of their own */
    accent-color: currentColor;
}
