/*
 * A composited card: art layer, frame image, live text.
 *
 * All inner positions come from card-geometry.json and are written as percentages by
 * CardRegion, so a card is correct at any size and needs no per-size recalculation.
 *
 * The card is a size container and all type is sized in `cqh`, so text fitted once
 * stays fitted when the card is scaled.
 */

/*
 * How much smaller than itself a card is currently being shown. The table sets it;
 * this file only consumes it, so it is declared here — the card lab loads card.css
 * without table.css, and an unset `--seat-scale` makes every calc() that reads it
 * invalid, which silently deletes the transforms that depend on it.
 *
 * Registered rather than left plain for two reasons: it computes to a real number, so
 * JS can read it back and convert screen distances into the card's own space; and it
 * gets an initial value, so a card rendered outside a table is simply at 1:1.
 */
@property --seat-scale {
    syntax: '<number>';
    inherits: true;
    initial-value: 1;
}

/*
 * The other half of the conversion, registered for the same reason: `CardScale` reads it
 * back to work out what a *held* card is being shown at, and an unregistered property
 * would hand it the literal text `calc(1 / var(--seat-scale))`.
 */
@property --present-scale {
    syntax: '<number>';
    inherits: true;
    initial-value: 1;
}

/*
 * --- Rendered large, shown small -----------------------------------------
 *
 * A card is laid out at the size it is READ at, and scaled DOWN to sit on the table.
 * Never the other way round.
 *
 * The table shows a card about 133 px wide; held up to be read it is about 550 px. Sized
 * to the table and magnified, that is a 4x blow-up of a raster the browser made at
 * table size, and it looked exactly like what it was — soft frame filigree and mushy
 * text at the one moment the copy is meant to be read. Nothing about the artwork was
 * at fault: the shipped frames are 768 px and the art 640 px, both comfortably larger
 * than the held card. The pixels were there; the card was just never rendered at a
 * size that used them.
 *
 * So the card's layout box IS the held size, and `.slot__mount` in table.css shrinks
 * it into its seat. Minification is cheap and clean; magnification is neither.
 *
 * The cost is that every absolute length inside a card is scaled down with it, so
 * lengths that are meant to look the same on screen have to be stated in the card's
 * own units. `cqh` is 1% of the card's height, which is ~200 px on the table — so 6cqh
 * is the 12 px these values used to be.
 */
.card {
    position: relative;
    width: var(--card-width, 250px);
    aspect-ratio: var(--card-aspect);
    container-type: size;
    user-select: none;

    --card-radius: 6cqh;
    --shadow-card: 0 6cqh 12cqh rgb(0 0 0 / 55%);
    --shadow-card-lifted: 0 12cqh 22cqh rgb(0 0 0 / 65%);

    /*
     * Held up, a card is shown at 1:1 with its own layout — the size everything inside
     * it was measured and rendered for. `--seat-scale` is how much the table shrank it,
     * so undoing exactly that is the whole definition of "brought forward to be read",
     * and there is no second number to keep in step with the first.
     */
    --present-scale: calc(1 / var(--seat-scale));
}

.card__inner {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    /* Perspective is applied here rather than as a `perspective` property on .card,
       which would fight the size containment that cqh units depend on.

       It is tied to the card's own height rather than being a fixed pixel value: at a
       table-size card a 1400px perspective is so distant that the rotation reads as a
       flat image swap, which is exactly how it looked before. Scaling it with the card
       keeps the same amount of foreshortening at any size. */
    --perspective: calc(var(--card-width, 250px) * 6);
    transform: perspective(var(--perspective)) rotateY(0deg);
    transition: transform var(--duration-flip) var(--ease-deal);
}

.card--face-down .card__inner {
    transform: perspective(var(--perspective)) rotateY(180deg);
}

/*
 * --- Turning a card over -------------------------------------------------
 *
 * Turning a card over IS picking it up. There is no separate reveal ceremony, and the
 * one that used to be here is worth describing because removing it was the fix: the card
 * came forward, turned, held still for two and a half seconds, and went back down on a
 * timer. It looked good and it was in the way. The instant a player sees a card is the
 * instant they know whether they want to seal it or wander from it, and that was the one
 * moment the interface took the card away and made them click it again.
 *
 * So the turn ends in the same held state as picking up a card that is already face-up
 * (`.card.card--inspecting` below). The travel is that state's own transition; the turn
 * is `.card__inner`'s. Both start together, and the delay below is the only thing
 * co-ordinating them.
 */

/*
 * The turn waits for the card to arrive, and then waits again.
 *
 * Two separate reasons, which is why this is a sum rather than a number. `--duration-
 * inspect` is the travel: turning during it reads as two effects fired at one object
 * rather than as a card being picked up and then turned over. `--duration-anticipate` is
 * the beat after it lands, and that one is the point — the back of the card is family art
 * and the only clue the player has about what is coming, and at table size they have
 * never seen it at a readable size. Turning on arrival puts it on screen for a few frames
 * and throws it away.
 */
.card--turning-up .card__inner {
    transition-delay: calc(var(--duration-inspect) + var(--duration-anticipate));
}

/*
 * Turning in place, used when the whole table is turned over at once — the shortcut for
 * someone who does not want to turn five cards one at a time. It stays a cascade with no
 * hold and no travel, because ten cards each coming to the reader would be a queue.
 */
@keyframes card-turn-in-place {
    0% { transform: perspective(var(--perspective)) translateY(0) scale(1) rotateY(180deg); }
    28% { transform: perspective(var(--perspective)) translateY(-9%) scale(1.24) rotateY(180deg); }
    72% { transform: perspective(var(--perspective)) translateY(-9%) scale(1.24) rotateY(0deg); }
    100% { transform: perspective(var(--perspective)) translateY(0) scale(1) rotateY(0deg); }
}

.card--turning-in-place .card__inner {
    animation: card-turn-in-place var(--duration-turn) var(--ease-deal) both;
}

.card--turning-in-place {
    z-index: 30;
}

/*
 * Holding a card up to read it, for as long as the reader wants.
 *
 * The single destination for both ways of getting here: turning a face-down card over,
 * and picking up one that is already face-up. It has no timer, because a timer that
 * expires mid-sentence is worse than no zoom at all — the card goes back when the reader
 * puts it back.
 *
 * Doubled class deliberately. `.card--dealt` lives in table.css, which loads *after*
 * this file, so at equal specificity its resting transform won the cascade and clicking a
 * card dimmed the table without moving the card at all.
 */
.card.card--inspecting {
    transform: translate(var(--present-x), var(--present-y)) scale(var(--present-scale));
    box-shadow: var(--shadow-card-lifted);
    z-index: 45;
    will-change: transform;
    transition:
        transform var(--duration-inspect) var(--ease-deal),
        box-shadow var(--duration-inspect) linear;
}

/*
 * Putting it back down.
 *
 * **A transition declared on the state being entered only animates the way in.** The rule
 * above is the whole reason a card travels smoothly to the reader — and it is removed at
 * the same instant the class is, so the journey home had no transition at all and the
 * card teleported into its slot. It was invisible for a dealt card, whose slot is a few
 * centimetres away, and unmissable for the Destiny, which crosses the whole stage.
 *
 * So the return gets a state of its own, carried for exactly as long as the trip takes.
 * It has to be a separate class rather than a transition on `.card`: the deal drives the
 * same property from inline styles and `.card--dealt`, and a standing transform
 * transition underneath all of that would tween things that are meant to be placed.
 */
.card.card--putting-down {
    z-index: 45;
    transition:
        transform var(--duration-put-down) var(--ease-deal),
        box-shadow var(--duration-put-down) linear;
}

.card__face {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
    border-radius: var(--card-radius);
    overflow: hidden;
    box-shadow: var(--shadow-card);
}

.card__face--back {
    transform: rotateY(180deg);
}

.card__face--back img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* --- Art layer ---------------------------------------------------------- */

/*
 * Sits behind the frame, clipped to the measured art window. Nine of the ten
 * windows are within 5% of square and source art is generated square, so
 * cover-cropping loses almost nothing. `setting` is the exception at 0.82 and its
 * art is generated 4:5 to suit (decision D10 — frames are never regenerated).
 */
.card__art {
    position: absolute;
    overflow: hidden;
}

/*
 * The art fills the window rather than sitting at its intrinsic size. This used to need
 * saying twice: while D9 shipped two formats every image was wrapped in a `<picture>`,
 * an inline element with auto height, and the img's `height: 100%` resolved against
 * *that* instead of against the art window — artwork stopped reaching the bottom and the
 * transparent frame showed through. With one shipping format there is no wrapper left to
 * collapse, so the img is the only thing to size.
 */
.card__art img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
}

/* --- Frame -------------------------------------------------------------- */

.card__frame {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

/* --- Text --------------------------------------------------------------- */

/*
 * Zones are fixed boxes measured from the frame art. Overflow is hidden so a
 * fitting failure is visible as clipping rather than as text sprawling across the
 * artwork.
 */
/*
 * Zones are fixed boxes measured from the frame art.
 *
 * The padding is functional, not decorative — it reserves room for glyph ink that
 * extends past the line box, which `overflow: hidden` would otherwise clip. But it is
 * kept SMALL and expressed in em, and em padding competes with the type for the same
 * fixed height: the fitter grows the font, the padding grows with it, and the text
 * ends up markedly smaller. It buys ink safety, not margin, so it is sized to the
 * former only.
 *
 * At line-height 1.3 a serif's ascenders and descenders already sit inside the line
 * box, so the wrapping zones need only a hairline of vertical safety. The horizontal
 * padding matters more: italic flavour text overhangs its advance width.
 *
 * The title needs least of all, because single-line text is measured by true ink
 * extent rather than by its line box (see TextFitter).
 */
.card__zone {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    text-align: center;
    line-height: 1.3;
    padding: 0.05em 0.12em;
}

.card__zone--title {
    padding: 0.03em 0.06em;
}

.card__zone--title {
    font-family: var(--font-display);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* Single line: leading would only steal height the glyphs could use. */
    line-height: 1;
}

/*
 * Title lettering, matched to the painted titles on the source templates.
 *
 * Those titles are not flat gold. Sampling the glyph pixels of `character.png` down
 * their height gives a pale cream crown deepening to a dull gold foot:
 *
 *     11%  #E5C774      55%  #DAAF5D
 *     34%  #E9C87E      66%  #D2A552
 *     45%  #E2BC6F     100%  #C09544
 *
 * The stop at 0% is set brighter than the sample, because the measured top row is
 * antialiasing against the dark banner rather than the highlight itself.
 *
 * The fill is a gradient clipped to the glyphs, so the shadow has to come from
 * `filter: drop-shadow()` rather than `text-shadow` — with `color: transparent`, a
 * text-shadow paints behind a fill that is not there. drop-shadow works from the
 * rendered result, and does not affect layout, so the fitter still measures correctly.
 */
.card__zone--title .card__text {
    background-image: linear-gradient(
        180deg,
        #f3dc9b 0%,
        #f7e2a6 26%,
        #e9c87e 52%,
        #dab064 76%,
        #c99a48 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;

    /* Tight edge for definition, then a soft shadow straight down, as on the templates. */
    filter:
        drop-shadow(0 0.07cqh 0 rgb(0 0 0 / 55%))
        drop-shadow(0 0.16cqh 0.16cqh rgb(0 0 0 / 70%));
}

/*
 * Titles are set on one line and shrink to width rather than wrapping.
 *
 * The span must be left UNCONSTRAINED so its measured width is the true text width.
 * A `max-width: 100%` here clamps the measurement to the box, the fitter concludes
 * every title already fits, and long titles are silently clipped instead of shrunk.
 */
.card__zone--title .card__text {
    white-space: nowrap;
    /*
     * Optical centring. The title is all-caps with line-height 1, so its line box
     * reserves descender space that no glyph uses — flex centring the box therefore
     * leaves the capitals sitting visibly high in the banner. Nudging down by roughly
     * half the descender puts the caps on the banner's optical centre.
     *
     * **Unitless, and read by the fitter.** `TextFitter` has to reserve twice this at the
     * top and bottom, or it sizes a title to fit the box exactly and this then pushes it
     * out through the bottom of the banner — which is what happened to EXILE. It is a
     * multiple of the font size rather than an `em` length because a custom property is
     * substituted verbatim, so `0.07em` would reach JavaScript as the string "0.07em".
     */
    --optical-nudge: 0.07;
    transform: translateY(calc(var(--optical-nudge) * 1em));
}

/* Body and flavour wrap, so they take the box width and grow in height instead. */
.card__zone--body .card__text,
.card__zone--flavour .card__text {
    width: 100%;
}

.card__zone--body {
    font-family: var(--font-body);
    color: var(--colour-ink);
}

/*
 * The flavour quote is tinted to its family, taken from the name ribbon painted at the
 * foot of the frame — a little more identity per card than one shared ink.
 *
 * It is a gradient rather than a colour because `conflict`'s ribbon is not one colour:
 * it runs deep red at one end to deep blue at the other, and the quote fades across to
 * match. Every other family authors the same value at both ends and reads as flat, so
 * the split is data rather than a branch (see Design/Content/family-colours.json).
 *
 * Clipped to the glyphs, so — exactly as with the title — the shadow has to come from
 * `filter: drop-shadow()`: with `color: transparent`, a text-shadow paints behind a
 * fill that is not there.
 *
 * The fallbacks matter. card-palette.css is generated, and a card rendered without it
 * must still show its quote rather than a row of transparent glyphs.
 */
.card__zone--flavour {
    font-family: var(--font-body);
    font-style: italic;
    color: color-mix(in srgb, var(--colour-ink) 78%, transparent);
}

.card__zone--flavour .card__text {
    background-image: linear-gradient(
        90deg,
        var(--family-ink-start, var(--colour-ink)) 0%,
        var(--family-ink-end, var(--colour-ink)) 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    /* Barely there. The quote is small and italic, and this is for definition against
       the parchment's texture, not for depth. */
    filter: drop-shadow(0 0.05cqh 0.08cqh rgb(0 0 0 / 22%));
}

.card__text {
    display: block;
}

/* --- Interaction -------------------------------------------------------- */

.card--interactive {
    cursor: pointer;
    transition: transform var(--duration-deal) var(--ease-lift);
}

.card--interactive:hover {
    transform: translateY(-0.75%) scale(1.02);
}

.card--interactive:hover .card__face {
    box-shadow: var(--shadow-card-lifted);
}
