/*
 * The table — the play surface, its slots, and the deal.
 *
 * Card sizing is driven by `--card-width` so the whole table scales from one value.
 */

/* --- Board geometry ------------------------------------------------------
 *
 * **THE NUMBERS ARE NOT HERE.** Every position on this board is measured in
 * `Design/Content/board-geometry.json` and compiled to `css/board-geometry.css` by
 * `build/build_board_geometry.py`. That file declares the `--board-*` properties this
 * one consumes, and owns the two `.table__row[data-row=...]` blocks outright, because
 * they contain nothing but placement.
 *
 * It is generated rather than typed because these numbers have to *agree with each
 * other*: a row's gap is the painted pitch minus the card that sits in it, and a card's
 * width is its recess height times two-thirds. Done by hand in a comment, that arithmetic
 * stops being redone the moment one input changes — which is exactly what happened. The
 * bottom row sat 4.3px off its outer recesses for a while, and the error presented as
 * `4.031` against `4.117`, which is not something anyone reads and catches.
 *
 * **The board is not flat, and three things depend on that.**
 *
 * 1. The bottom row is dealt at a **larger pitch and a larger card** than the top — 205.5
 *    against 202.1, and 205 tall against 202. That is a real forward tilt in the art, not
 *    noise: it is consistent across all five columns in both rows. Laying both rows out
 *    from one pitch put the outer four cards of the bottom row visibly off their recesses.
 * 2. The **ward sockets are not evenly spaced**, so every centre is written out rather
 *    than derived from a pitch. They are ellipses rather than circles for the same reason.
 * 3. The **deck well is a parallelogram**: its top and bottom edges are level to within a
 *    pixel while both sides lean 3.7 degrees. That is a skew, not a rotation — a rotated
 *    card would tilt its top edge against a level painted one, which is worse than not
 *    tilting at all.
 *
 * The recesses themselves are square-on, and that should stay: real perspective on a card
 * would fight the frames, which are final (D10).
 */
.table {
    /*
     * `--held-cqh` and `--render-cqh` are on `.stage` now, because the DESTINY panel and
     * the Attunement's two cards are siblings of this element and are rendered large and
     * scaled down by exactly the same rule.
     *
     * A fallback only. Both rows declare their own in `board-geometry.css`, fitted to the
     * painted recess's HEIGHT — never its width, because the recesses are 0.76 wide-to-tall
     * against the card's 2:3 and fitting to width would overhang top and bottom.
     */
    --seat-cqh: 14.311;

    --seat-scale: calc(var(--seat-cqh) / var(--render-cqh));
    --card-width: calc(var(--seat-cqh) * 1cqh);
    --card-render-width: calc(var(--render-cqh) * 1cqh);

    position: absolute;
    /*
     * NOT `overflow: hidden`, however tempting. A card held up to be read is centred on
     * the *stage* and stands 92cqh tall, so it reaches well outside this element —
     * clipping here to catch a layout overflow beheaded every card the player picked up.
     * Layout overflow is caught by looking at the captures instead.
     */
    inset: 0;
    z-index: 1;
}

/*
 * No longer a flex container.
 *
 * Every child is placed against the painted board, so they are positioned individually
 * and this element only exists to group them in the markup. `display: contents` removes
 * it from layout without removing it from the document, which keeps the absolute
 * children resolving against `.table` — the nearest positioned ancestor.
 */
.table__field,
.table__rows {
    display: contents;
}

/*
 * One row of five, placed by its own centre.
 *
 * The card size and the gap are declared HERE rather than inherited from `.table`,
 * because the two rows are not the same size — see the note at the top of this file. A
 * custom property is substituted where it is *declared*, so redefining `--seat-cqh` on
 * the row would not have changed a `--seat-scale` computed on `.table`: the derived
 * values have to be recomputed at the same level as the input they derive from.
 */
.table__row {
    position: absolute;
    display: flex;
    justify-content: center;
    gap: var(--slot-gap);
    /* Placed by its centre — half a card up, and half the row's width left. */
    transform: translate(-50%, -50%);

    --seat-scale: calc(var(--seat-cqh) / var(--render-cqh));
    --card-width: calc(var(--seat-cqh) * 1cqh);
}

/* Each row's size, gap and position are in `board-geometry.css` — see the note above. */

/*
 * The row has to be raised while it holds a card being read — the same trap as the mount
 * below, one level further out, and it was re-created the moment the row gained a
 * transform to centre itself on the board.
 *
 * `translate(-50%, -50%)` makes this element a stacking context, which traps the mount's
 * `z-index: 40` inside it. The row then paints at level 0, under the scrim at 20, and the
 * card the player picked up is dimmed and overprinted by the ward rows behind it. It is
 * loud in a screenshot and silent in the CSS, so: anything that raises a card must raise
 * every transformed ancestor between it and the scrim.
 */
.table__row:has(.card--inspecting),
.table__row:has(.card--turning-in-place) {
    z-index: 40;
}

/*
 * An empty optional row is *hidden*, not collapsed — it is positioned absolutely now, so
 * it reserves nothing either way. Worth knowing what this gives up: five empty painted
 * slots are a better advertisement for the deeper reading than the button is, so showing
 * the row's labels over the empty outlines is a live option once D8 settles.
 */
.table__row:empty,
.table[data-optional='hidden'] .table__row[data-row='optional'] {
    display: none;
}

/*
 * Pointing at the deeper reading fills the empty recesses with the names and questions it
 * would buy — see `Table.previewDeep`.
 *
 * The `display: block !important` is not shouting for its own sake: an unrevealed slot
 * carries the `hidden` attribute, and reset.css enforces that with `!important` precisely
 * so nothing can leave a hidden thing on screen by accident. Overriding it needs equal
 * weight and greater specificity, and this is the one place allowed to do it.
 */
.table[data-optional='preview'] .table__row[data-row='optional'] {
    display: flex;
}

.table[data-optional='preview'] .table__row[data-row='optional'] .slot[hidden] {
    display: block !important;
}

.slot--optional {
    transition: opacity 220ms ease;
}

.table[data-optional='preview'] .slot--optional {
    opacity: 0.82;
}

/* --- Slots --------------------------------------------------------------- */

/*
 * A fixed card-shaped box. The label is taken out of flow and hung underneath, because
 * the painted rows are only 34px apart: a label in the flow would push the seat off its
 * outline, and the seat landing on the paint is the whole point.
 */
.slot {
    position: relative;
    flex: none;
    width: var(--card-width);
    aspect-ratio: var(--card-aspect);
}

/*
 * The seat is a fixed-size well the card lands into. It keeps its footprint whether
 * or not a card is present, so nothing on the table shifts as cards arrive — the
 * single most common way a dealing animation ends up looking broken.
 *
 * It carries no outline of its own any more: the board paints one, and two nested
 * rounded rectangles a few pixels apart read as a rendering fault rather than as a slot.
 */
.slot__seat {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    border-radius: var(--card-radius);
}

.slot__question {
    max-width: 82%;
    text-align: center;
    font-family: var(--font-body);
    font-style: italic;
    font-size: 1.6cqh;
    line-height: 1.35;
    color: var(--colour-text-muted);
    text-shadow: 0 0.1cqh 0.5cqh rgb(0 0 0 / 85%);
}

.slot--occupied .slot__question {
    opacity: 0;
}

/*
 * Onto the board's own painted nameplate — the blank gold strip under each recess exists
 * for exactly this, which is why the brief asked for plates rather than painted names:
 * the position names come from `GET /api/spread` and change with the design, and a word
 * baked into a raster is invisible to a screen reader and frozen against the server.
 */
.slot__label {
    position: absolute;
    /* From the card's bottom edge, down to the plate's CENTRE, then back up by half the
       label's own height. Anchoring the label's *top* at a fixed drop is what left every
       one of them sitting low in its plate. */
    inset-block-start: 100%;
    margin-block-start: var(--board-plate-drop);
    inset-inline: -14%;
    transform: translateY(-50%);

    font-family: var(--font-display);
    letter-spacing: 0.1em;
    font-size: 1.35cqh;
    line-height: 1;
    white-space: nowrap;
    color: color-mix(in srgb, var(--colour-gold) 75%, var(--colour-text));
    text-align: center;
    text-shadow: 0 0.1cqh 0.6cqh rgb(0 0 0 / 90%);
}

.slot--revealed .slot__label {
    color: var(--colour-gold-bright);
}

/*
 * The mount is a full-size card box, shrunk to fit the seat.
 *
 * It exists so that the down-scale is applied *once*, outside everything the card
 * animates, and so the card itself never has to know it is being shown small. The
 * card fills the mount; the mount is centred on the seat and scaled to it, and
 * `scale()` about the centre keeps it centred at any factor.
 *
 * Nothing here animates. This is a layout scale, not a motion.
 */
.slot__mount {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    width: var(--card-render-width);
    aspect-ratio: var(--card-aspect);
    transform: translate(-50%, -50%) scale(var(--seat-scale));
}

/*
 * A transform creates a stacking context, so a card's own `z-index` can no longer lift
 * it past anything outside its mount — whatever raises the card has to raise the mount
 * with it. Without this a card held up to be read sits *behind* the scrim that is
 * dimming the table for it, and behind its neighbours.
 *
 * Keyed off the card's state rather than off a class the slot sets, so there is one
 * definition of "this card is raised" instead of two that can drift apart.
 *
 * Ordered low to high, and that order is load-bearing: `:has(.card--inspecting)` and
 * `:hover` weigh exactly the same, so the cascade settles them by which comes last.
 * Written the other way round, a card picked up under the pointer — which is every card
 * the player clicks — was raised to 3 rather than 40 and stayed under the scrim.
 *
 * A stray comment terminator used to sit in the middle of this block and close it early,
 * which turned the rest of the prose into an invalid selector — and CSS error recovery
 * discards an invalid rule TOGETHER WITH the block that follows it, so
 * `.slot__mount:hover` below was silently deleted. Nothing looked broken, because losing
 * the hover rule happens to remove the very conflict this paragraph is about.
 *
 * Do not write the terminator sequence inside a comment when describing this. Doing so
 * reintroduces the bug verbatim, which is exactly what happened on the first attempt to
 * document it.
 */
.slot__mount:hover {
    z-index: 3;
}

.slot__mount:has(.card--turning-in-place) {
    z-index: 30;
}

.slot__mount:has(.card--inspecting) {
    z-index: 40;
}

/* The card fills its mount exactly; the mount is what the layout sizes. */
.slot__mount .card {
    position: absolute;
    inset: 0;
    width: 100%;
    cursor: pointer;
}

/* --- Deal ---------------------------------------------------------------- */

/*
 * A dealt card is inserted already displaced toward the deck, then released. One
 * transform transition per card: nothing floats above the layout and there is nothing
 * to clean up if a deal is interrupted.
 *
 * The flight itself is NOT declared here. It is driven by inline styles from
 * DealChoreographer, because a class-based version of it looked correct and did
 * nothing — see the note there. This rule carries only what the flight does not set,
 * so there is no second, silently-wrong definition of where a card starts.
 *
 * The rest offsets are small and permanent — a table where every card sits exactly
 * square reads as a layout rather than as something that was dealt (Concept.md §9).
 */
.card--dealing {
    /* Rendered large and about to be scaled: tell the compositor before it rasters. */
    will-change: transform;
}

.card--dealt {
    transform:
        translate(var(--rest-x, 0), var(--rest-y, 0))
        rotate(var(--rest-rotate, 0deg))
        scale(1);
    opacity: 1;
}

/*
 * Straighten and lift: the card being considered is the one that should look picked
 * up, so hover cancels the resting jitter.
 *
 * The exclusions are load-bearing. This selector is more specific than the plain
 * `.card--inspecting` class, so without them a mouse resting
 * over a card would override the transform driving its animation and the card would
 * snap back to the table mid-reveal.
 *
 * The lift is a percentage of the card, not a length: a card is laid out at reading
 * size and scaled down, so an absolute length here would arrive on the table at a
 * quarter of its stated size.
 *
 * **It also grows a quarter.** Hovering is the half-step between glancing at a card on
 * the table and holding it up to read: not the full 4x of a card in the hand, just enough
 * that the title and the artwork resolve without committing to anything.
 *
 * The -16% is doing arithmetic, not styling. `scale(1.25)` about the centre pushes the
 * bottom edge down by an eighth of the card's height — which is 25px on the table, and
 * the painted nameplate is only 19px below it, so the card grows over its own position
 * name. Translating up by 12.5% cancels exactly that (percentages resolve against the
 * element's own unscaled box, and the scale is applied first), and the remaining 3.5% is
 * the lift. Moving the transform origin to the bottom would do the same job and would
 * jump, because the origin differs from the resting state's and is not interpolated.
 */
.slot__mount .card:not(.card--inspecting, .card--turning-in-place, .card--putting-down):hover {
    transform: translate(0, -16%) rotate(0deg) scale(1.25);
    transition: transform 200ms var(--ease-lift);
    z-index: 3;
}

/* --- Reading a card ------------------------------------------------------ */

/*
 * Everything behind a card being read is dimmed, and the dim is what catches the
 * click that puts it back — so dismissing works anywhere, not only on the card.
 */
.table__scrim {
    position: fixed;
    inset: 0;
    background: rgb(3 5 14 / 72%);
    opacity: 0;
    pointer-events: none;
    z-index: 20;
    transition: opacity var(--duration-inspect) linear;
}

/* One flag, because there is now one way a card ends up in the reader's hand. */
.table[data-inspecting='true'] .table__scrim {
    opacity: 1;
    pointer-events: auto;
}

/* --- Deck ---------------------------------------------------------------- */

/*
 * In the board's own deck well, bottom-left, under the DESTINY panel.
 *
 * NOT in that panel, though it is card-shaped and looks like an invitation — it was read
 * that way once. The panel is where the Act I archetype lands and stays for the whole
 * reading (D12), which is why it is card-shaped; the deck is only interesting for the two
 * seconds a spread is coming out of it, and permanent things earn painted homes first.
 */
.deck {
    position: absolute;
    /* The well's own hollow, which is not its geometric centre — its gilded lip is lit
       from the upper left, so the hole it draws sits below and right of the outline. */
    inset-block-start: var(--board-deck-y);
    inset-inline-start: var(--board-deck-x);
    /*
     * Skewed, not rotated. The painted well's top and bottom edges are level to within a
     * pixel while both of its sides lean 3.7 degrees — a parallelogram. Rotating the deck
     * to that angle would tilt its top edge against a level painted one, which reads worse
     * than leaving it square. The cards it deals are square-on, like their recesses; only
     * the stack sitting in this well is leaning.
     */
    transform: translate(-50%, -50%) skewX(var(--board-deck-skew));

    /*
     * Sized to the WELL, which is bigger than a dealt card — 165x230 against 155x202. Not
     * to `--card-width`: the well is what the eye compares this against, and a stack that
     * does not fill the thing drawn to hold it looks like a mistake rather than like a
     * smaller deck.
     */
    width: var(--board-deck-width);
    aspect-ratio: var(--card-aspect);
    /* Depth is drawn as stacked edges behind the top card; the count comes from JS so
       the stack visibly thins as the spread comes out. */
    filter: drop-shadow(0 1rem 1.5rem rgb(0 0 0 / 55%));
}

.deck__card {
    position: absolute;
    inset: 0;
    border-radius: var(--card-radius);
    overflow: hidden;
}

.deck__card img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.deck__edge {
    position: absolute;
    inset: 0;
    border-radius: var(--card-radius);
    background: linear-gradient(
        160deg,
        var(--colour-table-raised),
        color-mix(in srgb, var(--colour-table-deep) 80%, black)
    );
    border: 1px solid color-mix(in srgb, var(--colour-gold) 20%, transparent);
    /*
     * The cards *underneath*, so they show below and to the right — which is the same as
     * saying the top card is pushed up and to the left, the way a leaning stack settles.
     *
     * In `cqh` rather than pixels: the whole table scales with the stage, so a fixed 2px
     * is three times as much of the well on a small window as on a large one.
     */
    transform: translate(calc(var(--edge) * 0.2cqh), calc(var(--edge) * 0.24cqh));
    z-index: -1;
}

.deck[data-empty='true'] {
    opacity: 0.25;
}

/* --- Controls ------------------------------------------------------------ */

/*
 * On the shelf — the long plain strip the board reserves for exactly this. The deck and
 * the purse are children of this element in the markup but have wells of their own, so
 * they are placed individually and leave this row entirely.
 *
 * The shelf spans y 81.7-93.8cqh. The buttons take its upper half and the message line
 * below takes the lower, so the strip carries both the controls and what they just did.
 */
.table__controls {
    position: absolute;
    /*
     * Centred on the SHELF, not on the stage: its interior runs x 421-1320 of 1672, whose
     * centre is 52.06% rather than the board's 50%.
     *
     * That right edge was first measured at 1419, which is the painted ivy *beyond* the
     * shelf rather than the shelf's own frame — so everything on it sat about 50px right,
     * inside a strip whose left gap was three times its right one. Edge detection finds
     * the brightest thing in the window, and on this board the decoration outside a piece
     * of furniture is regularly brighter than the furniture. Check against the frame at
     * several heights and take the value that repeats.
     */
    inset-block-start: var(--board-shelf-y);
    inset-inline-start: var(--board-shelf-x);
    width: var(--board-shelf-width);
    transform: translate(-50%, -50%);

    display: flex;
    align-items: center;
    gap: 1.5cqw;
    justify-content: center;
}

/*
 * Deliberately quiet at rest and gold under the pointer.
 *
 * A gilded resting state was tried and backed out: the board is edged in gold everywhere,
 * so buttons that match it at rest stop announcing that they are the things you press.
 * The hover *change* is what says "pressable", and it needs somewhere to travel from.
 */
/*
 * Padded to fit FOUR of these, which is what the shelf now carries.
 *
 * The side padding is half what it was. At 2cqh the row ran off both ends of the strip
 * once Attune again joined it — and the width was never doing any work: every label here
 * is two or three words in display type, so the padding was buying air beside a phrase
 * that is already wider than any button needs to be. The vertical padding is untouched,
 * because that is what makes them look like things you press.
 */
.table__button {
    font-family: var(--font-display);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-size: 1.5cqh;
    padding: 1cqh 1cqh;
    border-radius: 0.4rem;
    border: 1px solid color-mix(in srgb, var(--colour-gold) 45%, transparent);
    background: color-mix(in srgb, var(--colour-table-raised) 70%, transparent);
    color: var(--colour-gold-bright);
    cursor: pointer;
    transition: background 180ms ease, border-color 180ms ease;
}

.table__button:hover:not(:disabled) {
    background: color-mix(in srgb, var(--colour-gold) 18%, transparent);
    border-color: var(--colour-gold);
}

.table__button:disabled {
    opacity: 0.4;
    cursor: default;
}

/* --- Motion is not optional ---------------------------------------------
 *
 * Talemancy deliberately does NOT honour `prefers-reduced-motion`.
 *
 * That setting is widely enabled as an operating-system preference for instant UI,
 * by people who are not asking a *game* to stop moving — and this is a game whose
 * whole proposition is cards being dealt and turned. A reduced-motion path here meant
 * cards materialising in place and reveals that blinked past before they could be
 * read, which is a worse experience than the motion it was avoiding, not a kinder one.
 *
 * Product decision, taken deliberately and with the trade-off understood: someone who
 * genuinely cannot tolerate motion is not well served by this product at all, and a
 * degraded version of it would not change that.
 */
