/* Study page. Loaded only by this project. */

/* Double-tap zoom off, for both pages of this project.

   The controls already opt out one by one, and that was not enough: a phone
   decides what a second tap means from where it landed, and the gaps between
   the buttons, the status lines, the scores and the whole photographed
   background behind them are all still page. Two quick presses that miss by
   a few pixels -- which is what happens when the buttons are small and the
   answer is obvious -- and the deck is magnified.

   Said here rather than in the shared stylesheet because it is not always
   the right answer. Double-tap zoom is useful on a page of text or source
   listings; it is only ever in the way on a page whose entire job is being
   tapped quickly. `manipulation` leaves pinch zoom and scrolling alone, so
   the reader can still magnify a card deliberately.

   A touch obeys every touch-action from the element it landed on upwards,
   which is what makes one declaration here cover the lot -- and why the
   controls in between still compute to `auto` when inspected. They are not
   missing a rule; the rule is on their ancestor. The card itself asks for
   less again (touch-action: pinch-zoom, further down) and keeps it: the
   stricter of the two wins where they meet. */
body {
    touch-action: manipulation;
}

/* Any author `display` outranks the browser's own [hidden] rule, and this file
   sets display on nearly everything it touches -- which is how the answer came
   to sit on screen with its hidden attribute set. Said once here, `hidden`
   can be trusted for the rest of the file. */
[hidden] {
    display: none !important;
}

/* The veil that goes over a deck's photograph, wherever one is used: on the
   cards of the index and behind the whole of a deck's own page.
   
   It follows the theme rather than always being dark. The photographs are all
   normalised to the same low brightness, which reads well under this site's
   light-on-dark text and not at all under the near-black text of its light
   theme -- so in that theme the veil is pale and the photograph sits behind
   the text rather than fighting it. */
:root {
    --photo-scrim: linear-gradient(rgba(251, 250, 248, .82), rgba(251, 250, 248, .82));
    --photo-card:  rgba(255, 255, 255, .70);

    /* Passed and missed want colour the shared palette does not carry. Both
       are written out per theme rather than derived: a green that reads
       against #fbfaf8 is not the green that reads against #16151a. On :root
       rather than on .study, because the deck list marks a finished deck with
       the same green and is not inside one. */
    --pass:      #2f6b46;
    --fail:      #9a3131;
    --pass-wash: #d6eddf;

    /* The veil a finished deck wears instead of the ordinary one: the same
       green wash a passed card turns, at the strength that leaves the
       photograph under it still readable as a photograph. */
    --photo-scrim-done: linear-gradient(rgba(214, 237, 223, .86), rgba(214, 237, 223, .86));
}

@media (prefers-color-scheme: dark) {
    :root {
        /* .70 put the veiled photograph at about the same brightness as a
           plain card, which made it a texture rather than a picture. .55
           leaves it visible and still carries the text. */
        --photo-scrim: linear-gradient(rgba(22, 21, 26, .55), rgba(22, 21, 26, .55));
        --photo-card:  rgba(30, 29, 36, .55);

        --pass:  #7fd1a0;
        --fail:  #f19a93;
        --pass-wash: #20402f;
        --photo-scrim-done: linear-gradient(rgba(32, 64, 47, .68), rgba(32, 64, 47, .68));
    }
}

.study {
    display: grid;
    gap: 1rem;
    justify-items: center;

    /* A card flying off to the right used to take the page with it. A transform
       does not move layout, but it does extend scrollable overflow -- and only
       rightwards: in a left-to-right page, overflow past the left edge is
       unreachable and counts for nothing, while overflow past the right edge is
       scrollable width. So the document grew by about a thousand pixels for the
       length of the animation and the phone resized to fit it, on a swipe right
       and never on a swipe left.
       
       clip rather than hidden: hidden would make this a scroll container and
       force the other axis to auto.

       The clip margin is the page's own side gutter, which puts the clip edge
       exactly at the edge of the screen. It matters: on a phone this container
       is the width of the card, so clipping at its own box would shave the card
       as it was dragged and make it look cut off rather than moving. At the
       gutter's width the card slides out through the edge of the screen, which
       is what it looks like it is doing, and the box-shadow is not shaved off
       at rest either. */
    overflow-x: clip;
    overflow-clip-margin: 1.25rem;
}

.study-controls,
.study-steps {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: wrap;
    justify-content: center;
}

/* One status line under the card, reading left to right as where you are and
   then how far you have got. They used to sit apart -- the position tucked on
   the end of the controls row, the tally alone at the bottom -- which put the
   two numbers describing the same deck in two different places. */
.study-status {
    margin: 0;
}

.study-progress {
    color: var(--ink-faint);
    font-size: .85rem;
    font-variant-numeric: tabular-nums;
}

/* A button whose whole content is an icon.
   
   Sized in rem rather than em so the glyphs stay the size they should be
   whatever the row's font is doing -- they carry the meaning here, and the
   labels they replaced were what stopped this row fitting on a phone. */
.button-icon {
    display: inline-flex;
    align-items: center;
    padding-left: .45rem;
    padding-right: .45rem;
    line-height: 1;
}

.button-icon svg {
    width: 1.2rem;
    height: 1.2rem;
    display: block;
}

/* Drawn in line rather than filled, so they read at this size and match each
   other. currentColor means they follow the button, including when it is
   disabled or the theme changes under them. */
.study-controls svg {
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* A switch has to look switched. The glyph changing is not enough on its own --
   you have to know the speaker without the waves is the off one, which is
   exactly the thing an icon should not need teaching. On is a filled chip; off
   is the plain quiet button the actions beside it use, so state reads before
   the drawing does.
   
   Accent-tinted rather than accent-filled: the direction pair below is already
   filled accent, and three solid chips in a row of five is a lot of shouting
   for two settings. */
.study-controls .button-icon[aria-pressed="true"] {
    background: var(--accent-soft);
    border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
    color: var(--accent);
}

/* The two sound switches carry a word as well as a glyph. A speaker cannot
   say which language it is about, and with two of them in a row the glyph
   alone would leave you guessing which was which. Small and tracked out: it
   is a tag on an icon, not a label in its own right. */
.button-voice {
    gap: .3rem;
}

.voice-tag {
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .04em;
    line-height: 1;
}

/* Which way round the cards are: two halves, and the one you are in is filled.
   aria-pressed is doing the work rather than a class, since it has to be set
   for the screen reader anyway and a second source of truth would only be a
   chance for the two to disagree. */
.dir-pair .button {
    font-weight: 650;
    letter-spacing: .03em;
}

.dir-pair .button[aria-pressed="true"] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--bg-raised);
}

/* And the half you are not in keeps an edge of its own, so the pair reads as
   two things to choose between rather than one filled chip with a word loose
   beside it. --rule alone disappears over a photograph. */
.dir-pair .button[aria-pressed="false"] {
    border-color: color-mix(in srgb, var(--ink-faint) 55%, var(--rule));
}

.button-quiet {
    background: transparent;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
}

button.button {
    font: inherit;
    font-weight: 550;
    border: 1px solid transparent;
    cursor: pointer;
}

.flashcard {
    display: grid;
    align-content: center;
    justify-items: center;
    gap: .5rem;
    width: min(100%, 34rem);
    min-height: 14rem;
    padding: 2rem 1.5rem;
    border: 1px solid var(--rule);
    border-radius: 1rem;
    background: var(--bg-raised);
    box-shadow: var(--shadow);
    color: inherit;
    font: inherit;
    text-align: center;
    cursor: pointer;

    /* Swiping. pinch-zoom means the browser handles pinching and nothing else,
       so a one-finger drag on this card is entirely ours.
       
       pan-y was the obvious choice and was wrong. It reserves vertical panning
       for the browser, so a swipe a few degrees off horizontal scrolled the
       page and moved the card at once -- and nothing in a pointermove handler
       can stop that, because scrolling answers to touch-action and to
       preventDefault on a non-passive touchmove, not to pointer events. The
       fix has to be in this property. Vertical dragging still scrolls: the
       script does it, since the browser no longer will.
       
       Double-tap zoom stays suppressed, as it is by any value but auto. The
       rest stops a drag turning into a text selection or a long-press menu. */
    position: relative;
    isolation: isolate;
    touch-action: pinch-zoom;
    /* The border carries "missed" the way the wash carries "passed", and like
       the wash it is driven from the finger's position rather than a clock.
       color-mix does the blend; where it is not understood the declaration is
       dropped and the border stays --rule, which is what it was before. */
    border-color: color-mix(in srgb, var(--fail) calc(var(--swipe-red, 0) * 100%), var(--rule));
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
}

/* The green a card turns: a layer under the text rather than a background on
   the card, so its opacity can be driven straight from the finger's position
   without touching any other colour. isolation above keeps the negative
   z-index inside the card, above its own background and below its content. */
.flashcard::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: var(--pass-wash);
    opacity: var(--swipe-green, 0);
}

/* No transition while a finger is down: the card tracks the finger exactly,
   and the green tracks its position rather than any clock. These two classes
   exist for the moments after the finger lifts -- settling back, or leaving. */
.flashcard.is-settling {
    transition: transform .22s cubic-bezier(.2, .8, .3, 1),
                border-color .22s ease-out;
}

.flashcard.is-settling::before {
    transition: opacity .22s ease-out;
}

.flashcard.is-flying {
    transition: transform .18s ease-in;
}

/* Wrong and Correct send the card off the same way a drag does, so the two
   ways of scoring a card look like the same act. */

/* Only where hovering is a thing that happens. On a touch screen :hover sticks
   to whatever was last touched until something else is, so a swipe left the
   card wearing this until the next tap -- an accent-coloured border sitting on
   a card for no reason anyone could see, and only after a swipe, never after
   pressing Wrong or Correct, because those leave the finger on the button
   instead. A mouse keeps the affordance; a finger never gets the stuck state. */
@media (hover: hover) {
    /* Not --accent. In the dark theme that is #ff9d6e and --fail is #f19a93,
       near enough the same colour that hovering an untouched card made it look
       like one you had missed. A neutral is the only thing a hover can say here
       without saying something else. Still blended with --fail, so hovering a
       card that really was missed does not wipe out the one thing its border
       was telling you. */
    .flashcard:hover {
        border-color: color-mix(in srgb, var(--fail) calc(var(--swipe-red, 0) * 100%), var(--ink-faint));
    }
}

.flashcard-side {
    font-size: clamp(1.5rem, 1.1rem + 2vw, 2.25rem);
    font-weight: 600;
    letter-spacing: -.02em;
}

.flashcard-back {
    display: grid;
    gap: .35rem;
    color: var(--accent);
}

.flashcard-back small {
    color: var(--ink-soft);
    font-size: .85rem;
    font-weight: 400;
    letter-spacing: 0;
}

.flashcard-mark {
    /* Holds its line whether or not there is a word in it, so scoring a card
       does not shift the one underneath. */
    min-height: 1rem;
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

@media (prefers-reduced-motion: reduce) {
    .flashcard.is-settling,
    .flashcard.is-settling::before,
    .flashcard.is-flying {
        transition-duration: 1ms;
    }
}

.flashcard-mark[data-status="pass"] { color: var(--pass); }
.flashcard-mark[data-status="fail"] { color: var(--fail); }

.button-correct {
    background: var(--pass);
    color: var(--bg-raised);
}

.button-wrong {
    background: var(--fail);
    color: var(--bg-raised);
}

/* Wrong and Correct are two halves of one decision, so they are drawn as one
   control: touching, with the join square and only the outer corners rounded.
   The borders are transparent and backgrounds paint under them, so the two
   fills meet with no seam and no negative margin needed.

   It also buys back a gap. Four separate buttons wrapped to two rows on a
   phone; three items in the row do not. */
.button-pair {
    display: inline-flex;
}

.button-pair .button:first-child {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

.button-pair .button:last-child {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}

/* Keep the row a row. The labels never break mid-word, and below about a
   phone's width everything gives up a little padding rather than a line.
   flex-wrap stays on as a safety valve: if a future label is longer than this
   arithmetic assumes, wrapping is a better failure than overflowing. */
.study-steps .button {
    white-space: nowrap;
}

/* Both rows stay rows on a phone.
 *
 * The upper four are the tight ones: at full size they want 463px on a screen
 * that has about 335 after the page gutters, and two of the four labels are
 * two words. Measured rather than guessed -- 1rem needs 463, .85rem needs 356,
 * .8rem needs 329, and .75rem needs 307. The last is the one taken, because
 * 28px of slack at 375px also covers a 355px phone, where 329 would not.
 *
 * They end up a shade smaller than the three below, which is right anyway:
 * these four are settings you touch occasionally, those are what you press on
 * every card.
 *
 * flex-wrap stays on. This is arithmetic about how wide eight English words
 * render, and if a label or a font changes, wrapping is a better failure than
 * overflowing the page. */
@media (max-width: 30rem) {
    .study-controls {
        gap: .25rem;
    }

    /* Four icons and a two-letter pair take a fraction of what five words did,
       so the shrinking this used to need is gone and only the gap is tightened. */
    .study-controls .button {
        padding: .35rem .45rem;
        font-size: .8rem;
    }

    .study-steps {
        gap: .4rem;
    }

    .study-steps .button {
        padding: .45rem .5rem;
        font-size: .85rem;
    }
}

/* The two directions on a deck's card in the list: the same pair the deck's own
   page shows, at the size a card can spare. A deck you have not started keeps
   the plain size instead -- "0 / 33" twice says nothing, and saying it on every
   untouched deck would bury the ones you are working through. Which of the two
   is shown is decided by decks.js, because only the browser knows. */
.deck-scores {
    display: grid;
    gap: .1rem;
    width: 100%;
    font-size: .68rem;
    line-height: 1.35;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}

.mini-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .6rem;
}

/* The direction you were last in, picked out the way the deck's own page picks
   it out, so that the small one and the large one read as the same thing. */
.mini-row.is-current {
    color: var(--ink);
    font-weight: 600;
}

.mini-count {
    white-space: nowrap;
}

.deck-scores.is-done .mini-count {
    color: var(--pass);
}

/* Category headings on the deck list. .section-head carries the look, shared
   with the section headings elsewhere on the site; this only pulls the first
   one up, since otherwise it sits a clear 2.5rem below a page heading that has
   just finished introducing it. */
.deck-category:first-of-type {
    margin-top: .5rem;
}

/* A deck's card on the index, with its photograph behind the words. The veil
   is the same one the deck's own page uses, so the two look like one thing. */
.card-photo {
    background-image: var(--photo-scrim), var(--deck-photo);
    background-size: cover;
    background-position: center;
}

a.card-photo:hover {
    border-color: var(--accent);
}

/* A deck finished both ways goes green -- the card, not just its numbers, so
   that what is done reads from across the list without being counted. It is
   the veil that changes rather than anything laid over the top, so the
   photograph stays as visible as it was. */
.card-link.is-done {
    border-color: color-mix(in srgb, var(--pass) 50%, var(--rule));
}

.card-photo.is-done {
    background-image: var(--photo-scrim-done), var(--deck-photo);
}

/* A deck with no photograph has nothing to tint but itself. */
.card-link.is-done:not(.card-photo) {
    background-color: var(--pass-wash);
}

/* On a deck's own page the card is translucent, so the photograph carries on
   behind it instead of stopping at its edge. The blur is what keeps the words
   legible over a photograph; where it is not supported the veil alone still
   does, which is why the colour is not more transparent than this. */
.study-photo .flashcard,
.study-photo .study-done {
    background: var(--photo-card);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}

/* The canvas the fireworks are drawn on. Over everything and catching nothing:
   a deck you have just finished should not become a deck you cannot click. */
.fireworks {
    position: fixed;
    inset: 0;
    z-index: 60;
    pointer-events: none;
}

/* How far through each direction you are. Two rows because there are two
   things to learn per card and they are not learnt together. */
.study-scores {
    display: grid;
    gap: .2rem;
    width: min(100%, 20rem);
    padding: .5rem .7rem;
    border: 1px solid var(--rule);
    border-radius: .5rem;
    background: var(--bg-raised);
    font-size: .78rem;
    color: var(--ink-faint);
}

.score-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
}

/* The one in front of you. The other stays legible rather than being dimmed
   away, since knowing it is behind is the point of showing it. */
.score-row.is-current {
    color: var(--ink);
    font-weight: 600;
}

.score-count {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Over a photograph it floats like the card does. */
.study-photo .study-scores {
    background: var(--photo-card);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}

.study-done {
    margin: 0;
    width: min(100%, 34rem);
    padding: 2rem 1.5rem;
    border: 1px dashed var(--rule);
    border-radius: 1rem;
    color: var(--ink-soft);
    text-align: center;
}

.button:disabled {
    opacity: .45;
    cursor: default;
}

.button:disabled:hover { border-color: var(--rule); }
