/* css/main/buttons-and-sheets.css — part 7/30 of the app styles (was css/main.css; loaded in this order by index.html). */
      /* ══ THE BUTTON FAMILY — one geometry, three fills ══════════════════════
         ⛔ WHY IT EXISTS (Santi, 2026-08-20). The primary one was Oswald 16 with no
         weight and the one next to it Inter 13/600, with different heights and
         radii: read together they didn't look like the same bar. Now the ONLY
         thing that changes between them is the fill.

         ⛔ NONE CARRIES ITS OWN MARGINS. `.btn-secondary` and `.btn-danger`
         had `margin-top: 8px` inside the class —meant for when they stack—,
         and in a row that pushed the button down 7 px and misaligned it. Santi saw
         it in the linked-shooters search: "the search button looks like it's
         sagging". The spacing comes from the CONTAINER, never from the piece.

         ⛔ UPPERCASE comes from the CSS, not from the translation text. Written
         inside the string it must be repeated in nine languages, and a language that
         doesn't want it can't undo it. */
      .btn-primary,
      .btn-secondary,
      .btn-danger,
      .btn-danger-solid {
        min-height: 48px;
        padding: 12px 18px;
        border-radius: 10px;
        border: 1px solid transparent;
        font-family: "Oswald", sans-serif;
        /* 14 px, the same size as the primary button in Score and in the
           directory. The scale jumps from 14 to 16 and no intermediate values
           are invented (§5): guard `unit-escala-visual`. */
        font-size: 14px;
        font-weight: 600;
        letter-spacing: 0.06em;
        text-transform: uppercase;
        cursor: pointer;
        transition:
          opacity 0.15s,
          transform 0.1s,
          box-shadow 0.15s,
          background 0.15s,
          border-color 0.15s,
          color 0.15s;
      }
      .btn-primary {
        width: 100%;
        background: var(--accent);
        color: var(--accent-fg);
        border-color: var(--accent);
        box-shadow: var(--elev-1);
      }
      /* ⛔ On press, the button DROPS flat: it loses its shadow. It's the only
         depth animation in the house and it means something —"I heard you"—.
         Just shrinking it was an effect with no meaning. */
      .btn-primary:active {
        opacity: 0.92;
        transform: scale(0.985);
        box-shadow: none;
      }
      /* ⛔ A DISABLED BUTTON MUST LOOK DISABLED, AND IT DIDN'T ON ANY
         SCREEN OF THE APP.
         There was no `:disabled` rule for the system buttons, so a disabled one
         was painted EXACTLY like an active one: you press it, nothing
         happens, and there's no way to know why. It's the "dead button" of
         [[reference_dead_buttons_toast_overlay]], and Santi found it on
         2026-08-30 in "Vinculaciones" —the button starts disabled until the
         checkbox is ticked, which is correct, but it wasn't visible—.
         ⚠️ It goes on the THREE families and not only the one that showed the
         problem: the flaw wasn't in that screen, it was in the system.
         ⚠️ And `opacity` with `cursor` and no shadow, not a fixed grey: a
         hardcoded colour wouldn't work in both themes (UX rule §2). */
      .btn-primary:disabled,
      .btn-secondary:disabled,
      .btn-danger:disabled {
        opacity: 0.45;
        cursor: not-allowed;
        box-shadow: none;
      }
      /* Neither "I heard you" nor a highlight on hover: there's nothing to hear. */
      .btn-primary:disabled:active,
      .btn-secondary:disabled:active,
      .btn-danger:disabled:active {
        transform: none;
        opacity: 0.45;
      }
      .btn-secondary {
        width: 100%;
        background: transparent;
        color: var(--text);
        border-color: var(--border);
      }
      /* ⛔ Only with a FINE pointer. On touch there's no "hover": the
         browser simulates it on tap and the background stays stuck until
         you tap elsewhere, which reads as "this is selected". */
      @media (hover: hover) and (pointer: fine) {
        .btn-secondary:hover {
          background: var(--hover-soft);
          border-color: var(--accent);
          color: var(--text);
        }
      }
      .btn-secondary:active {
        background: var(--hover-strong);
      }
      .btn-danger:active {
        background: var(--alerta-dim);
      }
      /* ⛔ The spacing between STACKED buttons comes from the CONTAINER, not the
         button (§8-ter i of the rules). It used to live as `margin-top: 8px`
         inside `.btn-secondary`, and that's why the button got misaligned as soon as
         it entered a row. Here: the dialogs, which stack Accept and Cancel. */
      .modal > .btn-primary + .btn-secondary,
      .modal > .btn-danger + .btn-secondary,
      .modal > .btn-danger-solid + .btn-secondary,
      .modal > .btn-secondary + .btn-secondary {
        margin-top: 8px;
      }
      .btn-danger-solid:active {
        opacity: 0.92;
        transform: scale(0.985);
      }
      /* ⛔ DELETE IS OUTLINED ON THE SCREEN, AND SOLID ONLY WHEN CONFIRMING
         (Santi, 2026-08-20). The solid red weighed more than the primary button of
         its own screen — i.e. what drew the eye most was what you must not
         press. In the confirmation it is the right action: `.btn-danger-solid` goes there.
         ⚠️ The red is `--alerta` for its role —it's the action to read twice—,
         no longer for contrast: when this was written `--danger` measured
         3.33:1 and wasn't readable, but since 2026-08-22 it is #ec7063 and also
         passes. So here it can now be chosen by design and not by obligation. */
      .btn-danger {
        width: 100%;
        background: transparent;
        color: var(--alerta);
        border-color: var(--alerta);
      }
      .btn-danger-solid {
        width: 100%;
        background: var(--alerta);
        color: var(--accent-fg);
        border-color: var(--alerta);
      }
      /* ══ THE ACCEPT/CANCEL BAR — STUCK AT THE TOP, IT DOESN'T LEAVE ═════════
         ⛔ SANTI'S RULE, 2026-08-20, and the two previous placements
         failed for the same reason: with the buttons at the TOP, at the end of a
         long form you have to scroll all the way up; with the buttons at the BOTTOM, if
         you change a field at the start you have to scroll all the way down. The problem
         wasn't which to choose — it was that the bar LEFT THE SCREEN.

         ⛔ TOP AND NOT BOTTOM, and it's not aesthetics: on mobile a bar stuck to the
         bottom edge collides with the nav bar (60 px) and above all WITH THE
         KEYBOARD —on iOS it covers or shifts it—, and the keyboard appears exactly when
         you're typing in the form, which is when the bar is needed.

         ⭐ AND THAT'S WHY IT CAN BE SMALL: if it's always in view it doesn't need to
         shout. 40 px tall, sized by content, not edge to edge.

         ⚠️ With words, not a ✓ and a ✕: a check next to a cross forces you to
         guess which is which, and rule §2 forbids meaning depending
         only on shape or colour.

         ⚠️ The OPAQUE background is mandatory: without it the form shows through
         under the buttons. The negative side bleed is set by the two rules
         below, and only for containers we KNOW
         have 16 px of padding. */
      .form-actions {
        position: sticky;
        top: 0;
        z-index: 2;
        display: flex;
        gap: 8px;
        justify-content: flex-end;
        align-items: center;
        margin: 0 0 12px;
        padding: 8px 0;
        width: 100%;
        background: var(--bg);
        box-shadow: var(--elev-1);
        box-sizing: border-box;
      }
      /* ⛔ THE BAR'S BACKGROUND IS THAT OF ITS SURFACE, not always `--bg`.
         All TWELVE `.form-actions` in the app live inside a `.modal`, whose
         background is `--surface`: with `--bg` you get an almost-black strip (#0d0d0d)
         over the dialog's grey (#1a1a1a), and it always shows —not only when
         scrolling— because the background is opaque.

         ⛔ AND THE RULE THAT FIXED THIS REACHED NONE OF THEM. It was
         `.modal > .form-actions`, a DIRECT child, and all twelve hang from
         `.modal-body`, so the selector never matched: the flaw was
         100 %, not an edge case. Measured on 2026-08-22 walking the DOM —
         all twelve at `rgb(13,13,13)` over `rgb(26,26,26)`. Santi: *"in dark
         mode it doesn't look right because it's a black and behind it is a dark
         grey"*. In light it was cream on white: less noticeable, which is why
         it had gone unseen.

         ⚠️ Descendant (` `) and not child (`>`) on purpose: the bar may
         hang from `.modal`, from `.modal-body` or from a form wrapper,
         and in all three it has the same surface behind it. */
      .modal .form-actions {
        background: var(--surface);
      }
      /* ⚠️ The side bleed ONLY where we know the container has 16 px
         of padding. In the modals it is NOT applied —they hang from `.modal-body`, which
         is not a direct child— and it must stay that way: with the background already unified, a
         bar reaching the edges would only make its shadow show
         edge to edge. */
      .section > .form-actions {
        margin-left: -16px;
        margin-right: -16px;
        padding-left: 16px;
        padding-right: 16px;
        width: auto;
      }
      .form-actions .btn-primary,
      .form-actions .btn-secondary,
      .form-actions .btn-danger,
      .form-actions .btn-danger-solid {
        flex: 0 0 auto;
        width: auto;
        min-height: 40px;
        padding: 0 18px;
        font-size: 14px;
      }
      /* ⛔ Action stack (not an accept/cancel bar): several
         actions of the same record, one below the other. NOT stuck to the top —it would be
         half the screen— and here full width is the right choice. */
      .form-actions-stack {
        display: flex;
        flex-direction: column;
        gap: 8px;
        margin: 0 0 14px;
        width: 100%;
        box-sizing: border-box;
      }
      /* Form explanation: same colour and size as .form-label (no uppercase).
         Thin separators above (after buttons) and below (before the fields). */
      /* ⛔ NO SEPARATOR LINES (Santi, 2026-08-20). It had `border-top` AND
         `border-bottom`, and it was a repeated habit: every time there was an
         explanatory text it got a rule above and another below. His
         words, looking at the linked-shooters search —where the rule
         above stuck to the identifier field—: "they smudge,
         they stain… they don't look good. If an explanatory text is needed, put it,
         but not with those lines".
         SPACE does the separating, not a rule: grey text at 12 px already
         reads as secondary without boxing it in. A rule is only
         used when it truly separates two distinct zones of the screen. */
      .form-explain {
        font-size: 12px;
        color: var(--muted);
        line-height: 1.5;
        margin: 8px 0 14px;
      }
      /* ⚠️ Inside a sheet body the gap comes from the container (`gap`),
         so its own margin ADDS UP and spaces double. It's cancelled here and
         the rule above is left alone, as it still holds where the explanation sits
         loose on a screen. It came with the 2026-08-24 move, which lowered the
         explanation into the body so the keyboard wouldn't eat the form. */
      .modal-body > .form-explain { margin: 0; }
      .modal-overlay {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.78);
        z-index: 50;
        display: none;
        align-items: flex-end;
        overflow: hidden;
      }
      .modal-overlay.open {
        display: flex;
      }
      /* LAYER 2 — floats. A sheet that rises from below with no shadow looks
         glued to the glass; with it you understand something is waiting underneath. */
      .modal {
        background: var(--surface);
        box-shadow: var(--elev-3);
        border-radius: 14px 14px 0 0;
        padding: 6px 16px 28px;
        width: 100%;
        max-height: calc(92dvh / var(--tl-zoom, 1));
        overflow-y: auto;
        overscroll-behavior-y: none;
        animation: slideUp 0.22s ease;
      }
      /* ══ THE SHEET WITH ITS OWN BODY: header STILL, body that scrolls ═══════
         ⛔ WHY, and it's a real flaw, not a preference. With the actions bar in
         `position: sticky` INSIDE a sheet that also resizes
         when the keyboard opens, on the tablet the window started going up and down by itself,
         non-stop and with no way to act (Santi, 2026-08-20, registering the
         club's device). Three things move at once there: the browser
         bringing the focused field into view, `_applyAppH` recalculating the
         sheet's `max-height` on each viewport event, and the sticky element
         repositioning after each one.

         ⭐ THE FIX REMOVES THE VARIABLE, it doesn't compensate: if the sheet is a COLUMN
         —handle, title, buttons and explanation on top, and `.modal-body` with the scroll—,
         the buttons are always in view **by construction**, with no `sticky` and
         nothing to reposition. It's what Santi asked for ("that they don't scroll, that I have them
         always on screen") and it's simpler too.

         ⚠️ 11 of the app's 12 windows already had `.modal-body`; the class existed
         and had NOT ONE line of CSS, so it did nothing. */
      .modal:has(> .modal-body) {
        display: flex;
        flex-direction: column;
        overflow: hidden;
      }
      .modal:has(> .modal-body) > .modal-body {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
        overscroll-behavior-y: contain;
        /* The bottom space comes from the body, not the sheet: the sheet no longer scrolls
           and its `padding-bottom` would leave a dead gap under the keyboard. */
        padding-bottom: 12px;
      }
      /* ⛔ AND HERE THE BAR IS NOT STICKY: it's in the still part. `sticky` inside
         a container that doesn't scroll does nothing, but leaves the element in its own
         stacking context with the shadow on — a free edge in
         the middle of the header. */
      .modal:has(> .modal-body) > .form-actions {
        position: static;
        box-shadow: none;
      }
      /* Modals with sticky action buttons */
      .modal:has(> .modal-footer) {
        padding-bottom: 0;
      }
      .modal-footer {
        position: sticky;
        bottom: 0;
        background: var(--surface);
        padding: 10px 0 28px;
        display: flex;
        flex-direction: column;
        gap: 6px;
        border-top: 1px solid var(--border);
      }
      .modal-footer .btn-secondary,
      .modal-footer .btn-danger {
        margin-top: 0;
      }
      @keyframes slideUp {
        from {
          transform: translateY(100%);
        }
        to {
          transform: translateY(0);
        }
      }
      /* Confirmation/notice dialogs (appConfirm/appAlert): they enter with a
         fade, no slide. This way the buttons are in their final position from
         the first frame and the first press lands (the slide-up made
         the first tap fall under the still-rising button → double press). */
      .app-dlg .modal {
        animation: dlgFadeIn 0.16s ease;
      }
      /* `appConfirmTecleando` (H65): the accept button starts DISABLED until
         the name is typed. Without this it was painted like an enabled one and it seemed
         the button didn't respond. */
      .app-dlg .btn-danger-solid:disabled {
        opacity: 0.45;
        cursor: not-allowed;
      }
      /* Same fix for the static confirmation modal (#modalConfirm),
         used by showConfirm and by competition deletion (cvEliminarComp):
         with slide-up the first tap fell under the still-rising button and forced
         pressing twice. With a fade the button is fixed from the first frame. */
      #modalConfirm .modal {
        animation: dlgFadeIn 0.16s ease;
      }
      @keyframes dlgFadeIn {
        from { opacity: 0; }
        to   { opacity: 1; }
      }
      /* Removes the ~300 ms "double-tap-zoom" delay on buttons:
         without this, on Android/tablet the browser holds the FIRST press
         waiting for a possible double-tap zoom, and only registers the second
         ("you have to press twice" effect in the confirmation windows).
         `manipulation` allows two-finger pan/zoom but disables
         double-tap, which adds nothing on a button. */
      button,
      .btn-primary, .btn-secondary, .btn-danger,
      .modal button, .cv-listing-btn, .nav-btn, .chip, .drawer-item, .bottom-nav-btn {
        touch-action: manipulation;
      }
      .modal-handle {
        width: 36px;
        height: 4px;
        background: var(--border);
        border-radius: 6px;
        margin: 10px auto 18px;
      }
      .modal-title {
        font-family: "Oswald", sans-serif;
        font-size: 18px;
        letter-spacing: 1px;
        color: var(--accent);
        margin-bottom: 18px;
      }
      .confirm-msg {
        font-size: 14px;
        line-height: 1.6;
        color: var(--text);
        margin-bottom: 20px;
        padding: 12px;
        background: var(--card);
        border-radius: 10px;
        border: 1px solid var(--border);
      }
