/* THE TIP COMPONENT, IN ITS OWN FILE SO TWO PAGES CAN SHARE IT.
 *
 * It lived in css/public.css, which only the audience page loads. Then the
 * profile page grew a LIVE DEMO of the audience-facing tip button, and the
 * demo rendered unstyled: the right markup with none of the rules that make it
 * a button. A picture of a control that does not look like the control is
 * worse than no picture.
 *
 * Copying the rules into css/dashboard.css was the other option and it is the
 * thing this project keeps getting burned by. One file, two <link> tags, and
 * the demo cannot drift from the real thing because it IS the real thing.
 *
 * Loaded by request.html and by dashboard/index.html, and precached by
 * dashboard/sw.js. Adding a rule here reaches both pages.
 * =========================================================================== */

/* ===========================================================================
   THE TIP COMPONENT. One appearance, four placements.

   Everything here is painted from --action and --surface, which every preset
   already proves at AA through scripts/check-contrast.mjs.

   THE PRIMARY CONTROL WAS THE ONLY THING IN THE APP CARRYING A GRADIENT, and
   as of 25 August 2026 it is not. The AI treatment at the foot of
   css/dashboard.css carries one too, so "it has a gradient" is no longer what
   makes this recognisable as itself. Three things now do, together:

     this control    top to BOTTOM, from --action, on a filled 48-52px button
                     that spans its container
     the AI mark     LEFT to right, a fixed blue no theme's --action can be,
                     on a 20px pill that never spans anything

   Direction, hue and size. tests/ai-treatment.mjs checks the pair rather than
   trusting this paragraph, and it reads the direction out of THIS file — so
   changing `to bottom` below without changing that suite fails it.
   =========================================================================== */

/* -------------------------------------------------------------- the coin */

/* Replaces the banknote everywhere. Round, unambiguous, survives 20px, and
   carries a pound rather than a dollar. */
.tip-coin { width: 100%; height: 100%; fill: currentColor; display: block; }

/* --------------------------------------------------------- the inline row */

/* A FULL-WIDTH BUTTON, AND IT USED TO BE A ROW WITH A SMALL CONTROL ON IT.
 *
 * Mark's verdict on the row: "very small, surely a full width button would be
 * better, at the moment it is too fiddly". The whole row was tappable and
 * nothing said so, so what a person actually aimed at was a 44px control on the
 * right-hand edge, one-handed, in a dark room.
 *
 * It is still a row in the page content: it scrolls, it is never sticky, never
 * floating, never dismissible, and it is unchanged after somebody tips. Only
 * its size and weight changed. */
.tip-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: 60px;
  margin-top: calc(22px * var(--density));
  padding: 0 18px;
  border: 0;
  border-radius: var(--radius-lg);
  background-color: var(--action);
  color: var(--action-text);
  font-size: 17px;
  font-weight: var(--weight-strong);
  box-shadow: var(--shadow-1);
}
.tip-row-glyph {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  color: currentColor;
}
.tip-row-label { flex: 0 1 auto; }
.tip-row-sub {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-secondary);
  text-align: center;
}

/* ------------------------------------------------------------- the card */

/* THE CARD IS A CARD, AND IT IS LIT FROM ABOVE IN THE MUSICIAN'S COLOUR.
 *
 * It was a run of controls flowing out of the paragraph above it, so on the
 * thank-you screen the amounts read as part of the confirmation rather than as
 * a separate offer.
 *
 * The tint is the same device as the control it contains, one step quieter:
 * enough that the card is the one object on the page wearing the musician's
 * colour, not enough to read as a coloured panel. An accent-FILLED card was
 * drawn in all seven themes and rejected: it reads as a cookie bar on Navy, a
 * promotional banner on Ember and Amber, and it burns the accent, so the
 * primary control inside it has to fall back to a plain slab and the
 * disclaimer becomes small print inside an advert.
 *
 * The bottom stop mixes toward --surface-raised rather than lightening,
 * because "9% lighter" is meaningless on Cream and Mono where that token is
 * already #FFFFFF and there is nothing above it.
 *
 * Costs the primary control inside it 4.28 to 3.40 in the worst pairing, Plum
 * with hot pink, against a floor of 3.00. A 25% tint was measured at 3.03 and
 * rejected: that is not a margin, it is a coincidence. */
/* VERSION B, CHOSEN BY MARK FROM THE SEVEN-THEME SHEETS: the card is the
 * musician's accent, edge to edge, and every control on it is one of the page's
 * own two planes. The controls read as holes punched through the coloured slab
 * to the page behind it rather than as objects sitting on it.
 *
 * This replaces the 8%-to-2% tint above. The tint was chosen to keep a
 * primary control legible on top of it; with the slab at full accent the
 * controls stop being tinted at all and take --surface and --bg instead, which
 * is what makes the arithmetic easier rather than harder.
 *
 * MEASURED ACROSS ALL 21 ACCENT-AND-THEME PAIRS, and re-measured in the app
 * after building it rather than only on the mock-ups: --surface clears the
 * accent at 5.08:1 worst case, --bg at 4.68:1, the pay control's worst gradient
 * stop at 4.05:1, and --text-primary clears all three at 11.40:1 or better.
 * The floor for a control against the surface behind it is 3:1. */
.tipcard {
  text-align: left;
  margin-top: calc(18px * var(--density));
  padding: 18px 16px 16px;
  background-color: var(--action);
  background-image: none;
  border: 1px solid var(--action);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
}
/* On the accent, the label colour is the accent's own ink. Set on the card
   rather than on each element so nothing inside it can be left in the page's
   text colour by accident. */
.tipcard .tipcard-head { color: var(--action-text); }
.tipcard .tipcard-why { color: var(--action-text); opacity: .88; }
.tipcard .tip-back,
.tipcard .tip-custom-label,
.tipcard .tip-custom-prefix { color: var(--action-text); }
/* IN THE DIALOG THE CARD KEEPS ITS COLOUR, and that is a change version B
 * forced rather than a preference.
 *
 * This used to make the card transparent inside the dialog, on the reasoning
 * that the card IS the dialog there and should not draw a second box inside the
 * dialog's own. That was right while the card was a faint tint. It is wrong now
 * the card is a full accent slab, because every colour on it is chosen against
 * that slab: the labels are the accent's ink, the chips are --surface and the
 * pay control is --bg. Strip the accent away and the ink lands on --surface,
 * the chips land on the same colour as themselves and disappear, and the whole
 * card is unreadable in exactly one of the three places it appears.
 *
 * So the card is the card everywhere. Only the spacing and the shadow are
 * dropped: the dialog supplies both, and doubling them draws a card inside a
 * card, which is what this rule was always actually for. */
.tip-dialog .tipcard {
  margin-top: 0;
  box-shadow: none;
}
.tipcard-head {
  margin: 0 0 4px;
  font-size: 20px;
  line-height: 1.2;
  font-weight: var(--weight-strong);
  text-align: center;
}
/* THE HEADING KEEPS OUT FROM UNDER THE CLOSE BUTTON.
 *
 * .tip-dialog-x is absolutely positioned 6px from the top-right of the dialog
 * card and is 48px square, so it occupies the first 54px of the card's right
 * edge — exactly the band the first heading sits in. The heading is centred in
 * a box that runs the full width of its container, and a long enough one puts
 * its own ink under the button.
 *
 * MEASURED, at 375px, in the tip dialog. short_name is capped at 24 characters
 * by artists_short_name_len, so "Tip Bartholomew Fairground" is a name a
 * musician can save today: its last 10px sat under the ×, and in the slab font
 * "Tip Wolverhamptonshire Brass" wrapped to two lines and still put 8px of the
 * FIRST line under it. Not a rounding artefact and not something you need a
 * ruler for: the descender of the "g" was plainly beneath the glyph.
 *
 * SYMMETRIC, so the heading stays optically centred. Inset on the right alone
 * would clear the button and move every heading 22px off centre to do it.
 *
 * 44px CLEARS BOTH CONTAINERS. The heading appears in two: inside .tipcard,
 * whose own padding already gives it 36px, and directly in .tip-dialog-body for
 * the played modal, which gives it 19px. The button needs 55. 44 is enough for
 * the tighter of the two with 8px to spare at 1440px, where the card is wider
 * and the button is not.
 *
 * A LONG NAME NOW WRAPS INSTEAD OF SLIDING UNDER THE BUTTON, which is what the
 * tip row at the bottom of the page has always done with the same name. */
.tip-dialog .tipcard-head { padding-inline: 44px; }
/* AND THE MODAL'S SECOND LINE, WHICH IS ALSO IN THE BUTTON'S BAND.
 *
 * The played modal puts a heading AND a line above the card, both as direct
 * children of .tip-dialog-body, so its body line is the second thing down and
 * still inside the 54px the button occupies. "Nice one. Send them a tip if you
 * fancy." ran 9 to 10px under the × at 375px, in every theme. Caught by the
 * check added to tests/tip-flow-review.mjs for the heading, on its first run,
 * which is the argument for measuring glyphs rather than eyeballing a
 * screenshot: it is the full stop and the "y" that were underneath.
 *
 * DIRECT CHILD ONLY. Inside .tipcard the same class sits below the heading and
 * clear of the button, and insetting it there would wrap "Tips go direct to the
 * musician." onto two lines on every screen to fix a modal. */
.tip-dialog-body > .tipcard-why { padding-inline: 44px; }
.tipcard-why {
  margin: 0 0 16px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--text-secondary);
  text-align: center;
}
/* THE PAY SCREEN'S TWO-LINE SUM, laid out as a checkout summary.
 *
 * TWO COLUMNS OF max-content, CENTRED AS A BLOCK. The pair is centred under the
 * heading, and inside it the figures are right-aligned to each other and the
 * labels left-aligned to each other. Right alignment is what makes it read as a
 * sum: both amounts carry two decimals (moneyExact() in js/tip.js), so aligning
 * their right edges puts the decimal points under one another.
 *
 * tabular-nums because the figures are a column. Four of the five fonts a
 * musician can pick have tabular digits anyway; the handwritten stack does not,
 * and without this "£5.00" over "£0.50" comes out visibly ragged.
 *
 * NO SEPARATE RULE FOR THE FEE LINE. Splitting it into the card network's share
 * and ours would be three lines to explain one number and would name a company
 * to somebody who has no relationship with it. Both fees leave the payment
 * together, so they are one line. */
.tipcard-why.is-sum {
  display: grid;
  grid-template-columns: max-content max-content;
  justify-content: center;
  column-gap: 14px;
  row-gap: 2px;
  font-variant-numeric: tabular-nums;
}
.tip-sum-amount { text-align: right; font-weight: var(--weight-medium); }
.tip-sum-label { text-align: left; }

/* FOUR ACROSS AT 375px, IN BOTH MODES. Measured inside the tip DIALOG, which is
   the width that actually matters and is not the width the earlier note used:
   the dialog card is 305px at a 375px viewport, so the grid is 271px and each
   chip is 61.8 x 56 with an 8px gap — not the 71.3 measured on the page card.
   Comfortably clear of the 44px floor in both directions. */
.tip-amounts {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}
.tip-amount-main { display: block; }
/* AUTOMATIC MODE ONLY. THE FEE, not the total, and the difference matters.
 *
 * This line used to print what the person pays: a £5 chip read "£5" over
 * "£5.50". Two figures, one of them a slightly bigger version of the other, and
 * people read neither. It now prints the DIFFERENCE, which is the one number
 * the big figure does not already tell them, and the total moved to the heading
 * of the pay screen where it is stated before anybody commits.
 *
 * ============ WHY THIS IS 11px AND CARRIES NO WORDS ============
 *
 * These chips used to go TWO BY TWO in automatic mode, because the line read
 * "You pay £10.68" and that does not fit four across — the first render clipped
 * it to "You pay £1(". Two by two fixed the clipping and cost the row. The
 * prefix went instead, and the row came back.
 *
 * THE SAME MEASUREMENT SAYS THE WORD "FEE" CANNOT COME BACK WITH IT. Measured
 * in the dialog, at 375px, in all five fonts a musician can pick. Four across
 * gives each chip 47.8px of inner width. The fee on the fourth DEFAULT preset
 * (£20) is £1.37; the fee on the largest preset anybody can set (£500) is
 * £29.31:
 *
 *                +50p fee   +£1.37 fee   +£29.31 fee   +£29.31   +£29.31 @10px
 *   sans           47.3       54.8  X      62.1  X       42.5        39.0
 *   condensed      39.7       44.4         51.6  X       35.3        32.1
 *   serif          44.9       54.0  X      60.1  X       42.8        38.9
 *   handwritten    34.4       44.5         50.0  X       37.5        34.1
 *   slab           48.3  X    57.3  X      63.8  X       43.7        39.7
 *                                                        ^^^^ fits everywhere,
 *                                                        4.1px spare at worst
 *
 * "+50p fee" clears four of the five fonts and fails slab by half a pixel.
 * "+£1.37 fee" fails three of five ON A STOCK ACCOUNT. Dropping to 10px does
 * not rescue the word either: "+£29.31 fee" is 57.1px in sans at 10px. So the
 * word goes and the figure stays, which is the same trade the prefix lost.
 *
 * 11px over 12px because 12px leaves slab 1.8px of headroom on the widest
 * figure and 11px leaves it 4.1px — a margin that survives a font fallback.
 *
 * SMALLER TYPE IS ALSO THE RIGHT HIERARCHY. The big number is the CHOICE and
 * this is its CONSEQUENCE; they should not compete.
 *
 * WHAT THE WORD WOULD HAVE DONE, SOMETHING ELSE DOES. "Fee" is said once,
 * above the grid (autoWhy in js/punter-copy.js), and again on each chip's
 * aria-label, which reads "£5 to them, 50p fee, you pay £5.50" — all three
 * figures and the noun, for anybody listening rather than looking. */
.tip-amount-fee {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  font-weight: var(--weight-normal);
  opacity: .85;
  white-space: nowrap;
}
/* FOUR ACROSS IN AUTOMATIC MODE TOO. The extra height is the second line; the
   width is unchanged from selection mode. */
.tipcard.is-automatic .tip-amount { height: 64px; }
/* THE CHIPS HAD NO EDGE ANYONE COULD SEE.
 *
 * They are --surface-raised, and the card behind them became a 2 to 8 per cent
 * tint of --surface-raised, so the fill measured 1.07:1 against it: the same
 * colour, by construction. The whole job of saying "this is a button" fell to a
 * 1.5px --border hairline, and --border is solved against the PAGE, not against
 * this card. Measured across all seven themes it comes out between 1.07 and
 * 1.89:1 against the card. Twenty-two rows below the 3:1 floor, on the primary
 * control of the screen, which is why the card read as faint.
 *
 * --border-strong is not enough either: 2.72 to 2.83:1 on the five dark
 * presets, which is close enough to look fixed and is not.
 * --action-on-surface is the token that is already solved against a surface for
 * exactly this reason, and it clears 5.08:1 at worst across all 21 preset and
 * accent pairs. */
/* TWO PLANES, ONE LADDER STEP APART, because that step is the only hierarchy
 * left once the card is a single flat colour. Chips take --surface; the pay
 * control takes --bg, so the primary control is the page itself showing through
 * and the chips sit one step off it. Same way round on light and on dark. */
.tip-amount {
  height: 56px;
  min-width: 44px;
  border-radius: var(--radius);
  border: 1.5px solid var(--surface);
  background: var(--surface);
  color: var(--text-primary);
  font-size: 18px;
  font-weight: var(--weight-strong);
}
/* A RING, NOT A RECOLOUR. Filling the chosen chip with the accent would make it
   vanish into the card it is sitting on. The ring is drawn in the accent's ink,
   which is the one colour guaranteed to clear the accent behind it. */
.tip-amount[aria-pressed="true"] {
  background: var(--surface);
  border-color: var(--surface);
  color: var(--text-primary);
  outline: 3px solid var(--action-text);
  outline-offset: 2px;
}
.tip-other {
  width: 100%;
  min-height: 48px;
  margin-top: 10px;
  border-radius: var(--radius);
  /* Dashed in --surface, which is the colour of the chips above it: the dash
     reads as the outline of a chip that is not there yet, which is what
     "another amount" is. */
  border: 1.5px dashed var(--surface);
  background: transparent;
  color: var(--action-text);
  font-size: 15px;
  font-weight: var(--weight-medium);
}
.tip-custom-wrap { margin-top: 12px; }
.tip-custom-label { display: block; font-size: 14px; margin-bottom: 6px; }
.tip-custom-row { display: flex; align-items: center; gap: 8px; }
.tip-custom-prefix { font-size: 20px; color: var(--text-secondary); }
.tip-custom {
  flex: 1;
  min-height: 48px;
  padding: 0 12px;
  font-size: 17px;
}
/* THE ERROR ON THE ACCENT SLAB, NOT ON A PAGE PLANE.
 *
 * "Amounts run from £3 to £500." was painted in --negative-text, which the
 * palette solves against the page and the two cards and the well. This card is
 * none of those: .tipcard above is `background-color: var(--action)` edge to
 * edge, and the note there says every colour on it is chosen against the slab —
 * the labels take the accent's ink, the chips take --surface, the pay control
 * takes --bg. This line was the exception, and it was not a near miss.
 *
 * MEASURED 28 AUGUST 2026 ACROSS ALL TWENTY-ONE THEME AND ACCENT PAIRS AND ALL
 * THREE APP THEMES. Not one reached 4.5:1. The reported case was Ember's rust
 * at 1.14:1; the worst was Mono's ink at 1.00:1, where the message was
 * literally the same luminance as the card it sat on. Somebody who typed £2
 * was told nothing at all and left staring at a button that would not move.
 *
 * --negative-on-action is derived per theme AND per accent, because the plane
 * behind this is the artist's accent and that moves independently of the
 * preset. It starts from --negative and is pushed along L, hue and chroma
 * held, until it clears 5:1 against --action — so it is still red, and it is
 * red that can be read on all twenty-four palettes. See deriveAccent() in
 * js/themes.js and the matrix line in scripts/check-contrast.mjs.
 *
 * The fallback is what this rule used to say. A context that has not had the
 * preset written onto it — a mock-up, an older cached stylesheet — lands where
 * it landed before rather than on nothing at all. */
.tip-custom-error {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--negative-on-action, var(--negative-text));
  min-height: 0;
}
.tip-custom-error:empty { display: none; }

/* THE ONE GRADIENT IN THE APP, AND IT IS LIT FROM BOTH ENDS.
 *
 * `white 9%` alone is a fixed step in oklab, and the accent already sits
 * anywhere on the lightness axis, so how much gradient you actually got
 * depended entirely on which accent the musician picked. Measured across all 21
 * theme and accent pairs: 8.2 L* on Mono's ink and 1.0 on Ember's bone, where
 * the control is already almost white and there is no headroom above it. On the
 * pale accents the gradient this rule exists to create simply was not there.
 *
 * Shading the bottom as well gives every accent somewhere to go, and the two
 * ends balance each other: 6.3 to 9.7 L* on all 21, which is the 8 to 10 per
 * cent section 19.12 asked for, on all of them rather than on some.
 *
 * .tip-fee-option.is-bright IS NOT IN THIS LIST, and it was, and that was a
 * bug. It became an 18% tint rather than a fill, and a background-image beats a
 * background-color, so the tint never took effect and the 24px amount sat on a
 * full accent fill at 1.00 to 1.18:1 against it. The most important number on
 * the screen was invisible on every palette. */
.tip-row,
.tip-custom-go,
.tip-pay-now,
.tip-card-toggle.is-only {
  background-image: linear-gradient(
    to bottom,
    color-mix(in oklab, white 9%, var(--action)),
    color-mix(in oklab, black 5%, var(--action))
  );
}
.tip-custom-go {
  width: 100%;
  min-height: 52px;
  margin-top: 10px;
  border: 0;
  border-radius: var(--radius);
  background-color: var(--action);
  color: var(--action-text);
  font-size: 16px;
  font-weight: var(--weight-strong);
}

/* VERSION B: THE CONTROLS ON THE CARD ARE THE PAGE, NOT THE ACCENT.
 *
 * Everything above defines these buttons as accent fills, which is right
 * everywhere the accent is NOT already behind them — .tip-row on the page is
 * the main one and is untouched. Inside .tipcard the card is now the accent, so
 * an accent button on it is a button you cannot see.
 *
 * --bg rather than --surface, one step below the chips, so the primary control
 * reads as the page itself showing through the slab. The gradient is kept and
 * re-based: the same two-sided 9%-white-to-5%-black shading, over --bg instead
 * of over the accent, so the control still has a lit top and a shaded bottom.
 *
 * MEASURED IN THE APP, IN ALL SEVEN THEMES, NOT ON THE MOCK-UPS. Worst case is
 * Cream and Plum at 4.05:1 for the lit stop against the accent behind it,
 * against a 3:1 floor; Mono, where the accent is near-black ink, is 14.26:1.
 * --text-primary on the control is 13.39:1 at worst.
 *
 * A TRAP WORTH RECORDING, because it nearly caused this rule to be rewritten
 * for no reason. color-mix() comes back from getComputedStyle as oklab(...),
 * not rgb(...). A contrast script that assumes three numbers in a colour
 * function are red, green and blue reads oklab(0.26 0.02 -0.03) as RGB 0,0,0
 * and reports the gradient as invisible. It reported exactly that: 1.22:1 on
 * Mono, which is not true of any pairing here. The fix is to have the browser
 * paint the colour and read the pixel back, which is what
 * scratchpad/tokens.mjs now does. Anyone re-checking these numbers must sample
 * painted pixels rather than parse computed strings.
 *
 * The outlined "Pay by card" joins them. Its border was 1.5px of --action,
 * which on a card that IS --action is a border drawn in the background colour.
 * There is nothing to outline against any more, so it becomes a fill like the
 * rest. */
.tipcard .tip-card-toggle,
.tipcard .tip-card-toggle.is-only,
.tipcard .tip-pay-now,
.tipcard .tip-custom-go {
  border: 0;
  background-color: var(--bg);
  background-image: linear-gradient(
    to bottom,
    color-mix(in oklab, white 9%, var(--bg)),
    color-mix(in oklab, black 5%, var(--bg))
  );
  color: var(--text-primary);
}

/* ---------------------------------------------------------- the fee step */

/* SIDE BY SIDE, SO THEY READ AS ALTERNATIVES.
 *
 * Stacked, the accent one on top read as a recommendation and the other as a
 * footnote to it, and the green one looked already chosen. Two columns is what
 * makes them two options.
 *
 * IT FITS AT 375px, measured: 375 page minus 32 padding minus 32 card padding
 * leaves 311, so two columns with a 10px gap are 150.5 each. That is above the
 * 44px floor in both directions with room for a two-line amount. */
.tip-fee-options { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-items: stretch; }
.tip-fee-option {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  width: 100%;
  min-height: 80px;
  padding: 12px 10px;
  border-radius: var(--radius);
  text-align: center;
  font: inherit;
  border: 1.5px solid var(--border);
  background: var(--surface-raised);
  color: var(--text-primary);
}
/* BRIGHTER, AND DELIBERATELY NOT FILLED.
 *
 * A solid accent fill reads as ALREADY CHOSEN, which is the one thing this
 * control cannot say: pre-selecting a charge beyond the main obligation would
 * breach regulation 40 of the Consumer Contracts Regulations 2013, which
 * requires express consent and states it cannot be inferred from a default left
 * unchanged. Looking preselected is close enough to being preselected that it
 * is not worth the argument.
 *
 * So it is a tint and a full-strength accent border: plainly the more prominent
 * of the two, plainly still a button nobody has pressed. The pressed state is a
 * different thing again — a ring and a tick, below. */
.tip-fee-option.is-bright {
  border-color: var(--action);
  border-width: 2px;
  background-color: color-mix(in oklab, var(--action) 18%, var(--surface-raised));
  color: var(--text-primary);
}
.tip-fee-option.is-bright .tip-fee-main { color: var(--action-on-surface); }
/* Slightly larger and heavier, and still plainly a button that needs pressing.
   The difference between the two is emphasis, not state. */
.tip-fee-option.is-bright { min-height: 88px; }

/* ===========================================================================
   THE GAP BETWEEN THE TWO WAS TOO SMALL TO SEE, AND IT READ BACKWARDS
   ===========================================================================

   `is-quiet` was `opacity: .95`. Five per cent is not a difference anybody can
   see, so the pair read as two equal boxes - and because the covering option
   prints its figure in the accent colour while the quiet one prints white, the
   covering option was reported as looking like the FADED one of the two. The
   emphasis was not just weak, it was inverted.

   So the quiet option is now plainly quieter: dimmer, flatter, no raised
   surface. And the covering option's figure is at full strength rather than
   tinted, with the accent doing its work in the border and the wash behind.

   WHAT THIS DELIBERATELY DOES NOT DO, and the reason is four paragraphs above
   this one rather than a matter of taste: it does not fill the covering option
   with a solid accent, and it does not make the quiet one look DISABLED. A
   control that looks already-chosen is close enough to being already-chosen to
   breach regulation 40, and a control that looks unavailable is not a free
   choice either. Both remain plainly legible, plainly pressable, plainly
   unpressed. Emphasis, not state, and both figures stay above AA against their
   own background - scripts/check-contrast.mjs is the check. */
.tip-fee-option.is-quiet {
  opacity: .78;
  background: var(--surface);
  border-color: var(--border);
}
.tip-fee-option.is-quiet .tip-fee-main,
.tip-fee-option.is-quiet .tip-fee-tag { color: var(--text-secondary); }
/* THE FIGURE CARRIES THE WEIGHT RATHER THAN THE COLOUR, and that inversion is
   the actual fault being fixed. It was printed in --action-on-surface, a light
   tint of the accent, against a background already tinted with the same accent.
   A light colour on a wash of itself is washed out, which is precisely how it
   was reported: the option meant to be the prominent one looked like the faded
   one. Full-strength text, a larger figure and a heavier weight, with the accent
   left to do its work in the border and the wash behind. */
.tip-fee-option.is-bright .tip-fee-main {
  color: var(--text-primary);
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.01em;
}
.tip-fee-option.is-bright .tip-fee-tag { color: var(--text-primary); }
/* Full strength, so the pair cannot read as equal. */
.tip-fee-option.is-bright { opacity: 1; }

/* TWO LINES EACH SINCE 24 AUGUST 2026, so the boxes came down from 96 and
   104px to 80 and 88. .tip-fee-sub was the third line and it restated the
   first two; the heights were sized around it and would otherwise have left
   16px of empty box under each pair. */
/* THE AMOUNT IS THE PROMINENT THING IN EACH. It is what a person is choosing
   between; the line underneath says what reaches the musician. */
.tip-fee-main { display: block; font-size: 24px; font-weight: var(--weight-strong); line-height: 1.1; }
.tip-fee-tag { display: block; font-size: 13px; font-weight: var(--weight-strong); line-height: 1.3; }

/* ONCE TAPPED, UNMISTAKABLE. A ring plus a tick, because a colour change alone
   is not a state on a control that was already coloured. */
.tip-fee-option[aria-pressed="true"] {
  outline: 3px solid var(--action-on-surface);
  outline-offset: 2px;
}
.tip-fee-option[aria-pressed="true"]::after {
  content: "\2713";
  font-size: 15px;
  font-weight: var(--weight-strong);
  line-height: 1;
}

/* Tapping the veil highlights these rather than doing nothing. */
.tip-fee-options.wants-attention .tip-fee-option {
  animation: tip-nudge .6s ease-out 2;
}
@keyframes tip-nudge {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50% { box-shadow: 0 0 0 4px color-mix(in oklab, var(--action) 45%, transparent); }
}

.tip-back {
  width: 100%;
  min-height: 48px;
  margin-top: 12px;
  border: 0;
  background: transparent;
  color: var(--action-on-surface);
  font-size: 15px;
}

/* ------------------------------------------------------- the pay controls */

/* WHILE THE WALLET IS ARRIVING.
 *
 * Stripe's Express Checkout Element takes several seconds to decide what it can
 * offer and draw it, and until now that was several seconds of nothing after
 * somebody had chosen an amount. Nothing on a payment screen reads as broken
 * faster than nothing.
 *
 * A skeleton the size of the wallet row, removed the moment the element reports
 * it has drawn or reports it has nothing to draw. It does not pulse: this is a
 * page that has just been asked for money and a throbbing rectangle is not the
 * note to strike. */
.tip-pay { margin-top: 14px; }
.tip-pay-loading {
  min-height: 55px;
  border-radius: var(--radius);
  border: 1.5px dashed var(--action-on-surface);
  display: grid;
  place-items: center;
  font-size: 14px;
  color: var(--text-secondary);
}
.tip-pay-loading[hidden] { display: none; }
.tip-pay-inner { position: relative; }
.tip-wallets { min-height: 55px; }
/* SITS OVER EVERY WAY TO PAY. Above Stripe's cross-origin iframe, which cannot
   be reached through it, and above our own card control, which could otherwise
   be pressed before a fee option had been chosen. That is what makes the
   element's internal starting amount unreachable rather than merely hidden. */
.tip-pay-veil {
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--surface) 72%, transparent);
  cursor: pointer;
  display: grid;
  place-items: center;
}
/* THE LINE THAT LETS ANYBODY TAP APPLE PAY.
 *
 * `hidden` is a presentation hint implemented as `[hidden] { display: none }`
 * in the user agent stylesheet, and a class selector setting `display: grid`
 * beats it on specificity. So a veil with hidden="" set was still laid out
 * across the pay controls: measured on the live site at 19,825 square pixels,
 * with elementFromPoint over the card button returning the veil's own label.
 *
 * The wallet button rendered, because it is an iframe underneath, and every tap
 * on it hit this instead. That is the whole of "Apple Pay still cannot be
 * tapped", through four attempts, and it is the SECOND time this exact
 * specificity trap has shipped: .tip-dialog did the same thing and swallowed
 * every tap on the page. tests/hidden-really-hides.mjs now fails the build if a
 * third one appears. */
.tip-pay-veil[hidden] { display: none; }
.tip-pay-veil-text {
  padding: 8px 16px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text-primary);
  font-size: 14px;
  font-weight: var(--weight-strong);
}
.tip-pay.is-veiled .tip-pay-inner > :not(.tip-pay-veil):not(.tip-pay-error) {
  filter: saturate(.3);
}
.tip-card-toggle {
  width: 100%;
  min-height: 55px;
  margin-top: 10px;
  border-radius: var(--radius);
  border: 1.5px solid var(--action);
  background: transparent;
  color: var(--text-primary);
  font-size: 16px;
  font-weight: var(--weight-strong);
}
/* When no wallet exists this is the only way to pay, so it stops being the
   quiet option. */
.tip-card-toggle.is-only {
  background-color: var(--action);
  color: var(--action-text);
}
.tip-card-fields { margin-top: 12px; }
.tip-pay-now {
  width: 100%;
  min-height: 55px;
  margin-top: 12px;
  border: 0;
  border-radius: var(--radius);
  background-color: var(--action);
  color: var(--action-text);
  font-size: 17px;
  font-weight: var(--weight-strong);
}
/* THE SAME PLANE AND THEREFORE THE SAME TOKEN. This one renders inside
   [data-tip-pay], which is inside .tipcard, so "your card was declined" was
   unreadable in exactly the same way and on exactly the same twenty-four
   palettes. Fixed with the amount error rather than left for later: they are
   one card, and half a fixed card is a card that still fails somebody. */
.tip-pay-error {
  margin: 10px 0 0;
  font-size: 14px;
  color: var(--negative-on-action, var(--negative-text));
}
.tip-pay-error:empty { display: none; }

/* THE DISCLAIMER, WHICH WAS STILL TOO FAINT TO READ.
 *
 * Plan section 20.6 asks for it below the offer and "prominent enough to
 * actually be read, not shrunk to a footnote". It was moved and enlarged and
 * still failed, because the CARD then became tinted too: the note's band is a
 * 10% accent mix over --surface and the card is an 8% mix over
 * --surface-raised, so the band all but vanished into the thing it sits on and
 * the whole box read as faint.
 *
 * It now has an edge of its own and a surface a step BELOW the card rather than
 * beside it, so it reads as a stated fact set into the card rather than as more
 * card. 15px at full text contrast, which it already had and which was never
 * the problem. */
/* AND IT IS NO LONGER INSIDE THE CARD AT ALL. See the note in js/tip.js beside
 * the markup: with the card at full accent and every control drawn from the
 * page's planes, a disclaimer kept inside the slab would be painted from the
 * same box of colours as the buttons. The line saying a tip buys nothing would
 * look like part of the thing selling it.
 *
 * So it is its own card on the page, in page colours, sitting under the offer
 * rather than within it. Mark chose this arrangement off the sheets and it is
 * the reason version B works: the objection to B was that the disclaimer stops
 * functioning inside an accent slab, and lifting it out is the answer to that
 * rather than a compromise with it.
 *
 * `.tipcard + .tip-note` — an ADJACENT SIBLING. If anything ever puts an
 * element between the card and this line, the line loses its styling and turns
 * into unstyled body text, which is loud enough to notice. That is deliberate:
 * silent degradation of the safety line is the one failure mode worth making
 * obvious. */
.tipcard + .tip-note,
.tip-inline > .tip-note {
  margin: 12px 0 0;
  padding: 12px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--action) 10%, var(--surface));
  font-size: 15px;
  line-height: 1.5;
  color: var(--text-primary);
  text-align: center;
}

.tipcard-thanks { text-align: center; padding: 8px 0; }
.tipcard-tick {
  margin: 0 0 6px;
  font-size: 34px;
  line-height: 1;
  color: var(--positive-text);
}

/* --------------------------------------------------------- the dialog */

/* A CENTRED CARD, NOT A BOTTOM SHEET. The Apple Pay sheet rises from the
   bottom edge, and two sheets from the same edge stack badly on iOS Safari.
   A centred card that the wallet sheet rises in front of is cleaner. */
.tip-dialog {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 16px;
}
/* THE LINE THAT STOPS A CLOSED DIALOG EATING THE WHOLE PAGE.
   `hidden` is a presentation hint implemented as `[hidden] { display: none }`
   in the UA stylesheet, and a class selector setting `display: grid` beats it
   on specificity. Without this the dialog stayed laid out across the viewport
   while invisible, and every tap on the page hit its scrim instead of the
   thing underneath. Caught by a test that could not press a button it could
   plainly see. */
.tip-dialog[hidden] { display: none; }
.tip-dialog-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, .55);
  opacity: 0;
  transition: opacity 150ms ease-out;
}
.tip-dialog.is-open .tip-dialog-scrim { opacity: 1; }
.tip-dialog-card {
  position: relative;
  width: 100%;
  max-width: 420px;
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  padding: 22px 18px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
  opacity: 0;
  transform: scale(.96);
  transition: opacity 180ms ease-out, transform 180ms ease-out;
}
.tip-dialog.is-open .tip-dialog-card { opacity: 1; transform: scale(1); }
.tip-dialog-x {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 48px;
  height: 48px;
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  font-size: 26px;
  line-height: 1;
}
html.tip-dialog-open, html.tip-dialog-open body { overflow: hidden; }

/* No bounce, no spring, and no motion at all for anybody who has asked for
   none. The card still appears; it simply appears rather than arriving. */
@media (prefers-reduced-motion: reduce) {
  .tip-dialog-card { transition: opacity 180ms ease-out; transform: none; }
  .tip-dialog.is-open .tip-dialog-card { transform: none; }
  .tip-fee-options.wants-attention .tip-fee-option { animation: none; }
}

/* The card never spans edge to edge. On a wide screen it stops rather than
   stretching, because a tip is a small thing and a full-width slab of it looks
   like an advert. */
.tip-inline { max-width: 460px; margin-inline: auto; }

/* ===========================================================================
   THE WHOLE CARD WAITS, OR NONE OF IT DOES

   The second screen used to arrive complete — heading, options, prices — with
   "Getting your payment options..." sitting in a gap in the middle of it. A
   hole in something otherwise finished reads as a fault, and this is the screen
   where somebody is about to hand over money.

   So while the quote is being fetched and Stripe is deciding what it can offer,
   the whole panel is one waiting state, and it becomes the finished card in one
   step. The skeleton is roughly the height of what replaces it, so nothing
   jumps when it goes.

   NOT A SPINNER AND NOT A PULSE. A throbbing rectangle is not the note to
   strike on a payment screen. It says what it is doing, in words, once.
   =========================================================================== */
.tipcard-fee { position: relative; }

/* THE FLOOR GOES ON THE PANEL, NOT ON THE COVER. It was on the cover, which is
   absolutely positioned: a min-height there made the cover taller than the box
   it was covering, and it spilled over the line of text underneath, which then
   showed through half obscured. Measured on the live card, where the panel is
   155px on the first paint and the cover was 190. The panel holds its own floor
   and the cover simply fills it. */
.tipcard-fee.is-preparing { min-height: 190px; }

.tipcard-fee.is-preparing > * {
  /* Still in the layout, so the card keeps its height and the page does not
     jump when the real thing arrives. Unreachable while it is covered. */
  visibility: hidden;
}

/* NO BORDER OF ITS OWN. It sits inside the tip card, which already has one, and
   a dashed rectangle drawn just inside a solid one reads as two panels rather
   than one waiting. The words and the plane are enough. */
.tipcard-fee.is-preparing::after {
  content: attr(data-preparing-text);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 24px 16px;
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 14px;
  line-height: 1.4;
  text-align: center;
  color: var(--text-secondary);
}

/* AND IT HAS TO MOVE, or it reads as stuck rather than as working.
 *
 * A SWEEP, NOT A PULSE. The note further up this file stands: a throbbing
 * rectangle is not the tone to strike on a page that has just asked somebody
 * for money. A highlight travelling in one direction along a track is read as
 * progress rather than as alarm, and it is the same idiom every payment sheet
 * uses, which is the point — it should be unremarkable.
 *
 * It is one element. The track is the background COLOUR and the travelling
 * highlight is a background IMAGE narrower than the box, moved across it, so
 * there is no second node to position and nothing that can drift out of the
 * bar.
 *
 * z-index because ::before paints below ::after by default, and ::after is the
 * opaque plane covering the card. Without it the bar is behind the very thing
 * it is meant to appear on. */
.tipcard-fee.is-preparing::before {
  content: '';
  position: absolute;
  z-index: 1;
  left: 50%;
  top: 50%;
  /* Below the centred text rather than beside it: the label is one short line
     and this clears it with room to spare if it ever wraps to two. */
  margin-top: 28px;
  transform: translateX(-50%);
  width: min(160px, 55%);
  height: 3px;
  border-radius: 999px;
  background-color: color-mix(in oklab, var(--action-on-surface) 20%, transparent);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    var(--action-on-surface) 50%,
    transparent 100%
  );
  background-size: 60% 100%;
  background-repeat: no-repeat;
  animation: tip-preparing-sweep 1.4s ease-in-out infinite;
}

@keyframes tip-preparing-sweep {
  from { background-position: -80% 0; }
  to { background-position: 180% 0; }
}

/* ASKED NOT TO BE MOVED ABOUT. The bar goes entirely rather than freezing:
   a stationary progress bar is a worse signal than none, because it reads as
   something that has stalled. The words carry it on their own. */
@media (prefers-reduced-motion: reduce) {
  .tipcard-fee.is-preparing::before { display: none; }
}

/* --------------------------------------------- already tipped at this gig

   ONE LINE WHERE A WHOLE TIP CARD USED TO BE.
 *
 * Every placement drew the full card with no idea whether this phone had
 * already paid, so somebody who tipped and then sent a second request got the
 * entire tip screen again on the confirmation. Asking twice in one evening is
 * the fastest way to make a musician switch the feature off, which costs them
 * every tip rather than one.
 *
 * The offer STAYS, because a second tip is allowed and hiding the control would
 * be the app deciding otherwise on somebody's behalf. It is simply at the
 * weight a second ask deserves: no coin, no gradient, no full-width button.
 * Text weight, next to the acknowledgement, in the accent this page already
 * uses for a quiet action (the same colour .tip-back takes).
 *
 * It sits in the same slots the loud version does, so nothing above or below it
 * has to know which one is being drawn. */
.tip-again {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 4px 10px;
  margin: 14px 0 0;
  font-size: 14px;
  line-height: 1.4;
  text-align: center;
}
.tip-again-note { color: var(--text-secondary); }
/* A REAL 44px TARGET around a text-weight label. The padding does the work and
   the negative margin keeps the line looking like a line rather than a control
   with a box around it. */
.tip-again-go {
  min-height: 44px;
  margin: -12px 0;
  padding: 0 4px;
  border: 0;
  background: none;
  color: var(--action-on-surface);
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.tip-again-go:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* The played modal's offer slot. Empty for somebody who has already tipped, and
   it must take no space at all when it is empty — an invisible gap above the
   close button reads as something that failed to load. */
.tip-modal-offer:empty { display: none; }
.tip-modal-offer { margin-top: 14px; }
