/* @version 20261003a */
/* The app layer: the shapes every signed-in page is made of.
 *
 * ot-layout.css holds the tokens - colour, radius, elevation, easing. This
 * holds the handful of things those tokens get assembled into, so that a card
 * on the report and a card on the dashboard are the same object rather than
 * two pages that arrived at a similar look.
 *
 * ---------------------------------------------------------------------------
 * Why this file exists
 *
 * The tokens were already right. --ot-r-sm/md/lg/xl and --ot-elev-1/2/3 are in
 * ot-layout.css with a comment explaining that radius is tied to size and that
 * "a 9px button inside a 16px card is the kind of mismatch that reads as an
 * accident, because it is one."
 *
 * Nothing used them. They were written for the builder and a test fenced them
 * to it, so every other page went on hardcoding its own: a survey of the seven
 * app pages found card radii of 8, 9, 10, 11, 12, 14, 16 and 18px, --ot-shadow
 * on two pages and no shadow at all on three others, and two entirely
 * different page columns - dashboard and orders run full width, while the
 * report, tickets and owner sit in a 1000px measure. That last one is what
 * somebody notices first: the report looked like a different product because
 * it was a third of the screen narrower than the page they came from.
 *
 * A token that nothing may use is a note, not a system. The point of this file
 * is that the assembled shapes live somewhere too.
 *
 * ---------------------------------------------------------------------------
 * How to adopt it
 *
 *   <link rel="stylesheet" href="/ot-layout.css">
 *   <link rel="stylesheet" href="/ot-app.css">
 *
 * Then delete the page's own copy of whatever it now gets from here. Adopting
 * it halfway is worse than not adopting it: two definitions of .ot-card, one
 * of them local and stale, is the bug this file is meant to end.
 *
 * See DESIGN-SYSTEM.md for what each piece is for and which page is on it.
 */

/* ---------------------------------------------------------------------------
 * The page column
 *
 * Two, because the app genuinely has two kinds of page and pretending
 * otherwise is what produced eight different widths.
 *
 * A WORKING page is a surface you do things on: tables, filters, charts,
 * anything where more width is more of the thing you came for. It runs the
 * width of the window. Dashboard and orders already did this and are the
 * reason it is the default.
 *
 * A READING page is one you read or fill in: a ticket, a receipt, a settings
 * form. Line length matters more than width there, so it keeps the 1000px
 * measure. --ot-measure has existed for this all along.
 *
 * The mistake was never having a name for the difference, so pages picked one
 * by accident.
 * ------------------------------------------------------------------------ */

.ot-app-main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* 28px, matching dashboard and orders. Not --ot-measure-pad (32px), which
     belongs to the measured column - a centred 1000px block can afford a
     wider gutter because it has margin either side of it anyway. */
  padding: 28px 28px 48px;
}

.ot-read-main {
  flex: 1;
  width: 100%;
  max-width: var(--ot-measure);
  margin: 0 auto;
  padding: 28px var(--ot-measure-pad) 48px;
}

/* A FORM page is a reading page with a shorter line. A label, a field and a
   line of help at 1000px is a long way for the eye to travel back, and the
   field ends up wider than anything anybody types into it.
 *
 * 880 is not a new number: organiser.html's settings form already used it,
 * and this is that value given a name so the next form does not pick its own.
 * Everything else is the reading column. */
.ot-form-main {
  flex: 1;
  width: 100%;
  max-width: 880px;
  margin: 0 auto;
  padding: 28px var(--ot-measure-pad) 48px;
}

@media (max-width: 640px) {
  /* 16 at the top, not 20. A phone screen is 956px at its tallest and the
     header above this has already taken 56 of them, so the space over a title
     is the one gap that costs a row of the list below it and buys nothing. */
  .ot-app-main,
  .ot-read-main,
  .ot-form-main { padding: 16px var(--ot-gutter, 16px) 40px; }
}

/* ---------------------------------------------------------------------------
 * The way back
 *
 * A second-level page is reached by pressing something on a list - a settings
 * tile, a night on the reports page - and a press that goes in needs a press
 * that comes out. The rail and the tab bar do not do it: they move you
 * sideways, between sections, and landing on Settings from Payments by way of
 * the rail is a different gesture that happens to arrive somewhere similar.
 *
 * It sits above .ot-page-head, reads as the quietest thing on the page, and
 * says where it goes rather than "Back" - which is the browser's word for a
 * button that is already there and means something else.
 * ------------------------------------------------------------------------ */

.ot-back {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  /* 40, not the 44 a control gets: it is a link in running text, and a row
     that tall above the title pushes the heading off the top of a phone. */
  min-height: 40px;
  color: var(--ot-muted);
  font-size: 14px;
  font-weight: 700;
  text-decoration: none;
}

.ot-back:hover { color: var(--ot-link); }

/* flex: none, or the chevron is squeezed to nothing beside a long label in a
   narrow column. */
.ot-back svg { width: 15px; height: 15px; flex: none; }

/* ---------------------------------------------------------------------------
 * The page head
 *
 * Title on the left, whatever acts on the page on the right, wrapping to two
 * rows rather than squashing. Every app page had built this and no two had
 * built it the same.
 * ------------------------------------------------------------------------ */

.ot-page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px 20px;
  flex-wrap: wrap;
  margin-bottom: 20px;
  /* The filter panel hangs off this on a phone, where it is too wide to hang
     off its own button. */
  position: relative;
}

/* flex-basis, so the title takes the row to itself the moment the controls
   beside it stop fitting - a heading that wraps mid-word next to a dropdown
   is the usual result of letting this shrink. */
.ot-page-head > :first-child { flex: 1 1 280px; min-width: min(100%, 220px); }

.ot-page-kicker {
  font-size: 12px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ot-muted);
  font-weight: 700;
}

/* 28px, because that is what the dashboard and orders already were. The
   report was clamp(28px, 2.6vw, 38px) and grew to 38 on a wide screen, which
   is a third bigger than the heading on the page you arrived from - part of
   why it read as a different product. Converging on the size two of the three
   pages already used is the smallest change that ends the disagreement. */
.ot-page-title {
  /* The site's heading face, at its one weight. A serif at 400 sits lighter
     than Figtree at 800 did, so it is a step larger to hold the same room.
     Loaded for the app pages by ot-site-theme.js, whatever display font the
     organiser chose for their public pages. */
  font-family: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  font-weight: 400;
  letter-spacing: -0.01em;
  font-size: 32px;
  margin: 0;
  /* A long event or series name has nowhere to break, and a heading that
     overflows its column takes the layout with it. */
  overflow-wrap: anywhere;
}

/* The gap belongs to whatever is above it, so a title with no kicker sits at
   the top of its box rather than floating four pixels down. */
.ot-page-kicker + .ot-page-title { margin-top: 5px; }

.ot-page-sub { font-size: 14px; color: var(--ot-muted); font-weight: 600; margin: 4px 0 0; }

/* 720, not 640.
 *
 * The note below says "a phone is the only width at which the row wraps",
 * and that was the assumption that broke: between 641 and 720 the head is
 * still a ROW, so it wraps exactly as described - the title on the first
 * line, whatever acts on the page pushed to the far right of the second,
 * with the row gap and the wrap between them. On the per-event report that
 * is the title, then a stretch of nothing, then "Yours $537.88" hard against
 * the right edge.
 *
 * 720 is the width the rest of the app already calls a phone: orders.html
 * and dashboard.html both switch their layouts there, and .ot-app-main's
 * gutter changes there. The head was the one piece still deciding at 640.
 */
@media (max-width: 720px) {
  .ot-page-title { font-size: 26px; }

  /* Left, like everything else in the column.
   *
   * .ot-panel-total is built for the right-hand end of a panel's head row,
   * so it carries text-align:right. Dropped into a left-aligned column it
   * keeps it, and the label ends up ragged over its own figure - "YOURS"
   * right-aligned above "$420.00" rather than starting where the title
   * starts. It is only a total at the end of a row while there is a row. */
  .ot-page-head > .ot-panel-total { text-align: left; }

  /* A column, not a row that wraps.
   *
   * Wrapped, the title takes the first line and whatever acts on the page
   * takes the second - and where each of them lands is then decided by the
   * auto space between them and by how the engine reads
   * `min-width: min(100%, 220px)` on a flex item that is not growing. Those
   * are two questions with no answer worth depending on, and a phone is where
   * they get asked, because a phone is the only width at which the row wraps.
   *
   * A column asks neither. The title starts at the gutter and runs the width
   * of the page; the controls sit under it; the only space between them is
   * the gap. Nothing here can put a 220px block against the right edge.
   */
  .ot-page-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    /* 14, not 20. On a phone the head is the second thing under a header the
       reader has already seen, and every pixel it spends is a pixel of the
       list it is the title of. */
    margin-bottom: 14px;
  }
  /* Stretch and basis belong to the row. In the column the title block is
     simply the width of the page. */
  .ot-page-head > :first-child { flex: none; width: 100%; min-width: 0; }
}

/* ---------------------------------------------------------------------------
 * Figures
 *
 * One class for any number the eye is meant to compare against another
 * number: a total, a count, a price, a cell in a numeric column.
 *
 * It exists because tabular-nums was already scattered across eleven files -
 * the same "every page decides for itself" that produced eight card radii,
 * one layer down. Half the figures in the app had it and half did not, and
 * which half was an accident of who wrote the page.
 *
 * Not for a date, a phone number or a reference code. Those are read, not
 * compared, and setting them in mono makes them look like something you are
 * supposed to add up.
 * ------------------------------------------------------------------------- */

.ot-fig {
  font-family: var(--ot-font-fig);
  font-variant-numeric: tabular-nums;
  /* Mono faces run wide. A touch back keeps a big figure from sprawling next
     to the label above it. */
  letter-spacing: -0.02em;
}

/* ---------------------------------------------------------------------------
 * Cards and panels
 *
 * A CARD is a small bordered box holding one thing. A PANEL is a card with a
 * head: a title, an optional line under it, and an optional total in the
 * corner. They are the same object at two sizes, which is why the padding
 * differs and nothing else does.
 *
 * Both take --ot-r-lg, because both are cards and the scale says cards are
 * 16px. Neither takes --ot-shadow: a bordered box on a tinted ground does not
 * need a shadow to sit on it, dashboard and orders never used one, and the two
 * pages that did were the two that looked out of place.
 * ------------------------------------------------------------------------ */

.ot-card,
.ot-panel {
  background: var(--ot-surface);
  border: 1px solid var(--ot-border);
  border-radius: var(--ot-r-lg, 16px);
  min-width: 0;
}

.ot-card { padding: 15px 17px 16px; }
.ot-panel { padding: 17px 19px 18px; display: flex; flex-direction: column; gap: 12px; }

/* Raised, for something that sits above the page rather than in it: a
   popover, a menu, a sheet. Not for a card in a grid. */
.ot-card-raised { box-shadow: var(--ot-elev-2); }

.ot-panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 14px;
}

.ot-panel-title { font-size: 14.5px; font-weight: 800; letter-spacing: -.01em; margin: 0; }
.ot-panel-sub { font-size: 12.5px; color: var(--ot-muted); font-weight: 600; margin: 2px 0 0; }

/* The figure a panel adds up to, in the corner a total goes in. */
.ot-panel-total { text-align: right; flex: none; }
.ot-panel-total span {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ot-faint);
}
/* The total a panel adds up to is a figure, so it is set like one. This was
   tabular-nums alone; the mono face is what makes a stack of panel totals
   down a page read as one column of money. */
.ot-panel-total b {
  font-size: 19px;
  font-weight: 800;
  font-family: var(--ot-font-fig);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

/* ---------------------------------------------------------------------------
 * Stats
 *
 * The row of figures across the top of a page. Separate cards with a gap
 * rather than one box with hairlines through it: hairlines read as a table and
 * the eye runs along them, but these are separate answers to separate
 * questions and each has to be findable on its own.
 * ------------------------------------------------------------------------ */

.ot-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
  gap: 12px;
}

.ot-stat { display: flex; flex-direction: column; gap: 2px; }
.ot-stat-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }

.ot-stat-k {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ot-faint);
}

/* tabular-nums everywhere a figure appears. Proportional digits make a column
   of numbers ragged, and these are read as a column even when they are a row. */
.ot-stat-v {
  font-size: 30px;
  font-weight: 800;
  line-height: 1.1;
  margin-top: 4px;
  font-family: var(--ot-font-fig);
  font-variant-numeric: tabular-nums;
  /* Tighter than .ot-fig's default: at 30px a mono face needs more pulling in
     than it does at 14. */
  letter-spacing: -0.035em;
}

/* The sub-line under a figure is prose about the figure, not another figure,
   so it goes back to the body face. Left in mono it read as a second number
   that had been cut off.
 *
   `inherit` is what this said first, and inherit is the mono: the parent is
   the figure. It has to name the body face, which is why --ot-font-body
   exists. */
.ot-stat-v small {
  font-family: var(--ot-font-body);
  font-variant-numeric: normal;
  letter-spacing: 0;
}
.ot-stat-v small {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--ot-muted);
  margin-top: 2px;
  letter-spacing: 0;
}

/* A landmark for the eye, not another figure. At full accent it competed with
   the number beside it. */
.ot-stat-glyph { width: 16px; height: 16px; flex: none; fill: var(--ot-accent); opacity: .55; }
/* The same landmark, drawn from the sprite rather than from a path handed in
   as a value. Every symbol in ot-icons.svg is an outline at a 24 grid, and
   several are built from <rect> and <circle> rather than a single <path> -
   which is what a d-attribute slot cannot carry. */
.ot-stat-mark {
  width: 17px; height: 17px; flex: none;
  fill: none; stroke: var(--ot-accent); stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  opacity: .7;
}

@media (max-width: 640px) {
  /* Two up, not one. auto-fit at a 178px minimum gives a single column on a
     375px phone, which is four screens of scrolling to read four numbers that
     are meant to be read together. */
  .ot-stat-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  /* An odd number of figures leaves the last one alone at half width with a
     half-width hole beside it - three on attendees, five on the box office.
     A tile is a card, and a row with one card and one gap in it reads as a
     card that failed to load. The last one takes the row instead. */
  .ot-stat-grid > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  .ot-card { padding: 12px 13px 13px; }
  .ot-stat-v { font-size: 24px; }
  .ot-stat-k { font-size: 10px; letter-spacing: .07em; }
}

/* ---------------------------------------------------------------------------
 * Figures inside a panel
 *
 * A panel that holds several figures puts each in its own tinted box rather
 * than laying them out loose. It costs vertical space and buys a clear answer
 * to "what belongs to what" - four numbers in a row under a heading read as
 * one run of text, and four boxes read as four answers.
 *
 * One step further from the card than the card is from the page, so the
 * nesting is legible as nesting: page, card, box. On a dark ground it goes
 * lighter instead, because there is nowhere darker to go.
 *
 * Not for two figures. A pair does not need boxes to be told apart, and the
 * edges start costing more than they explain.
 * ------------------------------------------------------------------------- */

.ot-nest-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
}

.ot-nest {
  background: var(--ot-nest-bg);
  border: 1px solid var(--ot-border);
  border-radius: var(--ot-r-md, 12px);
  padding: 13px 14px 14px;
  min-width: 0;
}

.ot-nest-k { font-size: 12px; color: var(--ot-muted); font-weight: 600; margin-bottom: 6px; }
.ot-nest-v {
  font-size: 18px;
  font-weight: 700;
  font-family: var(--ot-font-fig);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.ot-nest-s {
  display: block;
  font-size: 12px;
  color: var(--ot-faint);
  font-weight: 500;
  margin-top: 5px;
  line-height: 1.45;
}

/* ---------------------------------------------------------------------------
 * A grid of panels, and the empty state that keeps one from collapsing
 * ------------------------------------------------------------------------ */

.ot-panel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 12px;
}

/* Centred in whatever height the panel ended up with, so an empty panel
   beside a full one holds its side of the row instead of reading as broken. */
.ot-panel-empty {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 120px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ot-muted);
  text-align: center;
}

/* The space between one band of the page and the next. One value, so the
   rhythm down a page is even without each section choosing for itself. */
.ot-stack > * + * { margin-top: 22px; }

/* ── The switch ────────────────────────────────────────────────────────────
   Lifted from the builder's .ne-toggle, which is where it was drawn and
   argued over. The two colour choices in it are the argument:

   Off is --ot-muted - a plain token, and a solid mid-dark one in every scheme
   by construction. It was a color-mix over --ot-text with --ot-border-strong
   as the fallback, and --ot-border-strong is a hairline, so any browser
   without color-mix drew a near-white track under a near-white knob and no
   switch to speak of.

   The knob is --ot-surface, not white: a white knob falls under 3:1 on one of
   the twenty accents, and the surface colour clears it on all of them.

   On is --ot-accent rather than --ot-cta. The CTA pair is built to carry dark
   ink, and a switch has no ink on it, so the pair just read as unfilled. */
.ot-toggle {
  appearance: none; -webkit-appearance: none; margin: 0; flex: 0 0 auto;
  width: 44px; height: 26px; border-radius: 999px; cursor: pointer;
  position: relative; border: none;
  background: var(--ot-switch-off, var(--ot-muted));
  transition: background .15s ease;
}
.ot-toggle::after {
  content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--ot-switch-knob, var(--ot-surface));
  transition: transform .15s ease;
}
.ot-toggle:checked { background: var(--ot-accent); }
.ot-toggle:checked::after { background: var(--ot-surface); }
.ot-toggle:checked::after { transform: translateX(18px); }
.ot-toggle:focus-visible { outline: 2px solid var(--ot-accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .ot-toggle, .ot-toggle::after { transition: none; }
}

/* ---------------------------------------------------------------------------
 * A TABLE.
 *
 * This file said a table was "not a shape it has an opinion about yet", and
 * for a while that was honest: Orders was the only page with one. Attendees
 * made it two, and two pages drawing the same rows to two sets of numbers is
 * how the eight different card radii happened.
 *
 * So the opinion is here, taken from what Orders already drew: a heading row
 * in small caps, hairlines between rows rather than a box around them, and
 * the first and last cells flush with the page so the heading, the rows and
 * the tab above them all start at the same x.
 *
 * What is NOT here: column widths, minimum widths, and what a row becomes on
 * a phone. Those are about a particular table's columns - Orders turns its
 * nine cells into a card by hand - and belong to the page that has them.
 * ------------------------------------------------------------------------- */
.ot-table { width: 100%; border-collapse: collapse; }
.ot-table th {
  text-align: left; font-size: 12px; font-weight: 700; color: var(--ot-muted);
  text-transform: uppercase; letter-spacing: 0.4px; padding: 12px 16px;
  border-bottom: 1px solid var(--ot-border); background: transparent;
}
.ot-table td {
  padding: 14px 16px; border-bottom: 1px solid var(--ot-border);
  font-size: 13.5px; vertical-align: middle;
}
.ot-table th:first-child, .ot-table td:first-child { padding-left: 0; }
.ot-table th:last-child, .ot-table td:last-child { padding-right: 0; }
/* The row you are pointing at. Opt-in on the row rather than on every tr:
   Orders puts a grey band between one event's orders and the next, and a
   bare tbody tr:hover would light that band up as if it were a row you could
   act on. A row that is a row says so. */
.ot-table tbody tr.ot-row:hover td { background: var(--ot-surface-2); }

/* A numeric column. Right-aligned and set as a figure, on the th as well as
   the td so the heading sits over its own column rather than over the one to
   its left.
 *
 * This is where the mono face earns itself: four rows of money in the body
 * face are four slightly different widths, and the eye has to read each one
 * to compare them. In a column they line up on the decimal point and the
 * comparison is free. */
.ot-table th.ot-num, .ot-table td.ot-num {
  text-align: right;
  font-family: var(--ot-font-fig);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
/* The heading is a label, not a figure - it keeps the body face and only
   takes the alignment. */
.ot-table th.ot-num { font-family: inherit; letter-spacing: 0.4px; }

/* ---------------------------------------------------------------------------
 * A TABLE IN A CARD.
 *
 * The rules above put the first and last cells flush with the page, so the
 * heading, the rows and the tab above them all start at the same x. That is
 * right for a table that IS the page, and it is why the lists have looked
 * like a spreadsheet dropped onto a background since the ground went tinted:
 * a list with nothing around it has no edge, and the tint gave everything
 * else on the page one.
 *
 * So the wrapper becomes the card, which is what the lab mockups draw and
 * what both of these wrappers already half were - each carried
 * `border-radius:0; overflow:visible` with no background, which is a card
 * somebody turned off.
 *
 * The pager goes INSIDE it. It was a separate bar under the list already
 * carrying its own border-top; inside the card that border is the last rule
 * of the table rather than a line floating in the gap under it.
 *
 * Desktop only. On a phone both lists go full-bleed white and the rows become
 * stacked cards - a card around a column of cards is a box around a box - so
 * each page turns this off in its own phone block.
 * ------------------------------------------------------------------------- */
/* A DESKTOP shape, and the media query is here rather than in the two pages
 * that use it. Both restack their rows into cards on a phone, so a card round
 * the list would be a box round a column of boxes - but each page saying that
 * for itself is a race: _headers serves HTML and CSS with a week of
 * stale-while-revalidate, so a page cached before this landed, against a
 * stylesheet fetched after it, is a card nobody can turn off. One file owns
 * the shape and the width it applies at. */
@media (min-width: 768px) {
  /* No overflow:hidden.
   *
   * It had one, so a hovered first or last row could not paint over the
   * rounded corner. What it actually did was trap the row menu: the kebab
   * opens an absolutely positioned panel inside this card, so a row near the
   * bottom got ten pixels of menu and the rest clipped away. Reported as
   * "can't see the kebab menu", and it was every list on every page that has
   * one - the dashboard, Orders, Attendees.
   *
   * Nothing else needs the clip. The table head is transparent, so the top
   * corners have no fill running under them; the only thing that reaches an
   * edge is a hovered last row, and that is rounded below instead. */
  .ot-table-card {
    background: var(--ot-surface);
    border: 1px solid var(--ot-border);
    border-radius: var(--ot-r-lg, 16px);
  }
  /* The padding the flush rule took away. Without it the first column sits
     against the card's own border and the last runs into the opposite one. */
  .ot-table-card .ot-table th:first-child,
  .ot-table-card .ot-table td:first-child { padding-left: 16px; }
  .ot-table-card .ot-table th:last-child,
  .ot-table-card .ot-table td:last-child { padding-right: 16px; }
  /* The card's edge is the last rule. A border under the final row draws a
     hairline one pixel inside it, which reads as a misprint. */
  .ot-table-card .ot-table tbody tr:last-child td { border-bottom: 0; }
  /* The corners the clip used to hold. Only the last row can reach them: the
     head above is transparent, and anything drawn after the table - a pager,
     an empty state - brings its own ground. */
  .ot-table-card .ot-table tbody tr:last-child td:first-child {
    border-bottom-left-radius: var(--ot-r-lg, 16px);
  }
  .ot-table-card .ot-table tbody tr:last-child td:last-child {
    border-bottom-right-radius: var(--ot-r-lg, 16px);
  }
  /* A touch warmer than --ot-surface-2 on a white card, which is the same
     near-white the ground is mixed from and all but invisible against it. */
  .ot-table-card .ot-table tbody tr.ot-row:hover td { background: var(--ot-nest-bg); }
}

/* ---------------------------------------------------------------------------
 * AN EMPTY LIST.
 *
 * Inside the card, because a list with nothing in it is still the list - a
 * page that loses its box the moment it has no rows reads as having lost the
 * feature rather than as having none of it yet.
 *
 * Taken from orders.html, which already had the better of the two the app was
 * carrying: a mark, a line that names the state and a line that says what
 * fills it. The dashboard's was a rotated gradient rectangle standing in for
 * an icon and a button on accent-with-white, both written inline.
 *
 * The mark takes the same glyph the rail item does, from the same sprite at
 * the same weight - an empty Waitlist shows the clock that Waitlist shows.
 * ------------------------------------------------------------------------- */
.ot-empty {
  text-align: center; padding: 44px 20px 38px;
  display: flex; flex-direction: column; align-items: center;
}
.ot-empty-mark {
  width: 46px; height: 46px; border-radius: 12px;
  background: var(--ot-surface-2); color: var(--ot-faint);
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 15px;
}
.ot-empty-mark svg {
  width: 22px; height: 22px; display: block;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.ot-empty-title { font-size: 16px; font-weight: 800; color: var(--ot-text); letter-spacing: -.01em; }
/* The app layer's body size, which is what .ot-table td is set at. */
.ot-empty-body { font-size: 13.5px; color: var(--ot-muted); margin: 7px 0 0; line-height: 1.5; max-width: 42ch; }
.ot-empty-act { margin-top: 16px; }

/* ── The page head's controls ──────────────────────────────────────────────
 *
 * Search, the filter, and the tools all sit in the page head beside the
 * title, rather than in a bar above the list.
 *
 * A bar above the list is a band the width of the page holding three small
 * controls and a lot of air, and it pushes the first row down by its own
 * height on every page that has one. The head already has that room: the
 * title and its line take two lines of height on the left, and the right of
 * that row was empty. Putting the controls there costs the list nothing.
 *
 * It also settles where a control lives. Search, filter, select and export
 * are things you do TO the list, and they now sit together in one place on
 * every page, in the same order, instead of each page arranging its own.
 */
.ot-head-tools {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.ot-search { position: relative; display: block; width: 230px; max-width: 100%; }
.ot-search svg {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--ot-faint);
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  pointer-events: none;
}
/* 36px rather than the 44px a field usually gets: it is sitting in a row of
   40px tools beside a heading, not in a form being filled in. */
.ot-search input {
  width: 100%; height: 36px; box-sizing: border-box;
  padding: 0 12px 0 33px; font-size: 13.5px;
}

/* Export says its name where there is room for it.
 *
 * It is the one control in this row that people go LOOKING for rather than
 * recognise - the others are things you already know are there (search,
 * select, the filter). A tray-and-arrow on its own is a guess, and the
 * tooltip that named it only appears on a pointer, which is exactly the
 * device that has the room for the word.
 *
 * Still .ot-tool: the same height, the same hover, the same tooltip. What is
 * added is a border, a squarer radius and the label - so it reads as a button
 * among icons, which is what it is.
 */
/* .ot-tool.ot-tool-export, not .ot-tool-export: .ot-tool sets the border,
   the radius and the padding this overrides, and a bare modifier class ties
   with it - which leaves the cascade to decide on file order across two
   stylesheets. The same trap the phone row rules fell into. */
.ot-tool.ot-tool-export {
  border-color: var(--ot-border-strong);
  border-radius: 12px;
  /* No fill: the outline, like the tools beside it ("remove white background
     from all export buttons"). */
  background: transparent;
  color: var(--ot-text);
  padding: 0 13px;
}
@media (hover: hover) {
  .ot-tool.ot-tool-export:hover { border-color: var(--ot-accent); background: transparent; }
}
/* The word, not the button: at 720 and below the row is already four or five
   controls wide and the label is what gives. The glyph still says it, and the
   aria-label still names it. */
.ot-tool.ot-tool-export .ot-tool-word { font-size: 13.5px; font-weight: 700; }
@media (max-width: 720px) {
  .ot-tool.ot-tool-export { background: transparent; padding: 0 11px; }
  .ot-tool.ot-tool-export .ot-tool-word { display: none; }
}

/* A picker in the tools row. Attendees has one - the page is "who is coming
   to this one", so the event is not a filter that can be dropped, it is the
   subject. Sized with the search beside it rather than as a form field. */
.ot-head-select {
  height: 36px; box-sizing: border-box;
  padding: 0 30px 0 11px; font-size: 13.5px;
  max-width: 260px; text-overflow: ellipsis;
}

/* ── The selection pill ────────────────────────────────────────────────────
 *
 * What is ticked, and the one or two things worth doing to a set of them, in
 * a pill floating over the list.
 *
 * On both pages this was a full-width band above the table: it cost the list
 * a row of chrome for as long as select mode was on, and the count was off
 * screen the moment you scrolled down to tick anything - sticky pins a box
 * only once its own edge would scroll past, and this one sat above the rows
 * so it never moved. Floating, it costs the list nothing and sits where the
 * eye already is, over the rows being ticked.
 *
 * Orders arrived at this first and had it to itself. It is the same shape on
 * every list, so it lives here now.
 */
.ot-bulk {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 70;
  /* Clear of the home indicator, and of the shell's tab bar, which is drawn
     along this same edge and was covering the lower half of the pill.
     --ot-tabbar-h is what the shell publishes for exactly this, and it is
     only defined at the widths where the shell draws a bar - so the one
     declaration covers every width, and the fallback is what the pill is
     worth on a page standing on its own with nothing under it. */
  bottom: calc(24px + var(--ot-tabbar-h, env(safe-area-inset-bottom, 0px)));
  display: flex; align-items: center; gap: 14px;
  /* One line while one line fits. Shrink-to-fit sizes a fixed box to its
     widest CHILD rather than to the row, so Clear and Delete selected came
     out one above the other in a pill twice as tall as it needed to be.
     max-content asks for the row; the max-width still caps it, and the
     actions still wrap when the row genuinely does not fit. */
  width: max-content; max-width: calc(100vw - 32px);
  padding: 10px 12px 10px 18px; border-radius: 999px;
  background: var(--ot-surface); border: 1px solid var(--ot-border-strong);
  box-shadow: 0 16px 40px rgba(4, 8, 20, .22);
}
.ot-bulk-count { font-size: 12px; font-weight: 700; color: var(--ot-text); white-space: nowrap; }
.ot-bulk-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-left: auto; }

/* ── The filter ────────────────────────────────────────────────────────────
 *
 * Every value you can filter by, with a count beside it, in one panel - in
 * place of a row of dropdowns.
 *
 * A dropdown holds one value at a time and says nothing until you open it, so
 * three of them is three closed doors and no way to ask for two venues at
 * once. The counts are the other half: "Draft 2" answers the question a lot
 * of people were opening the dropdown to ask.
 */
.ot-filter { position: relative; display: inline-block; }
.ot-filter-btn.is-on { background: var(--ot-accent-soft); color: var(--ot-text); }
.ot-filter-count {
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--ot-accent); color: #fff;
  font-size: 11px; font-weight: 800; line-height: 18px; text-align: center;
}
.ot-filter-panel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
  width: 300px; max-height: min(70vh, 560px); overflow: auto;
  background: var(--ot-surface); border: 1px solid var(--ot-border);
  border-radius: 16px; box-shadow: 0 16px 40px rgba(4, 8, 20, .16);
  text-align: left;
  /* No padding of its own at the top: the first legend's own padding is the
     panel's edge, so STATUS sits the same distance under the panel's top as
     WHEN and WHERE sit under the rule above them. */
}
/* The air is on the legend and on the group's foot, none of it on the group's
   head - because a fieldset's padding-top lands BELOW its legend, not above
   it. The 16px here was meant to separate one group from the next and instead
   sat between a heading and the rows it heads: 24px under the word WHEN, with
   nothing at all above it. The heading read as belonging to the group above. */
/* min-width:0 - a fieldset is as wide as its widest content by default, and a
   long venue name pushed the counts off the right edge of the sheet. */
.ot-filter-group { border: 0; margin: 0; padding: 0 0 18px; min-width: 0; }
/* The title row is the phone sheet's; the dropdown has its button for that. */
.ot-filter-head { display: none; }
/* On the group, not the fieldset's own border: a <legend> interrupts the
   border-top of the fieldset it sits in, so a rule drawn that way came out as
   a line running off to the right of the word WHEN with a gap where the word
   is. A pseudo-element is not part of the fieldset's border box, so the
   legend has nothing to interrupt.
   It hangs off the BOTTOM of the group above rather than the top of the group
   below, which is the same line but lands in the right place. An absolutely
   positioned child resolves against its fieldset's padding box, and a legend
   is laid out above that box - so top:0 came out UNDER the word WHEN, putting
   the rule between the heading and the rows it heads. Nothing sits below a
   group's padding box, so bottom:0 has no such trap. */
.ot-filter-group { position: relative; }
.ot-filter-group::after {
  content: ''; position: absolute; bottom: 0; left: 0; right: 0;
  height: 1px; background: var(--ot-border);
}
.ot-filter-group:last-of-type::after { content: none; }
.ot-filter-legend {
  /* 18 above the word, 6 below: the heading belongs to the rows under it, and
     the air goes between the groups. Its own padding is the only way to put
     space above the word - the legend is laid out above its fieldset's
     padding box, and the rule is drawn at the top of that box. */
  /* The eyebrow of every pop-up (Pop-ups 3): 12px, 700, .06em. */
  padding: 18px 20px 6px; font-size: 12px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ot-muted);
}
.ot-filter-row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 10px 20px; cursor: pointer;
  font-size: 14px; font-weight: 600; color: var(--ot-text);
}
@media (hover: hover) { .ot-filter-row:hover { background: var(--ot-surface-2); } }
.ot-filter-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* The figure face, so a column of counts lines up down the right. */
.ot-filter-meta { font-family: var(--ot-font-fig); font-size: 12.5px; color: var(--ot-muted); }
.ot-filter-foot {
  position: sticky; bottom: 0;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 20px; border-top: 1px solid var(--ot-border);
  background: var(--ot-surface);
}
/* Clear is the secondary button beside the primary (Pop-ups 11). It was a
   line of faint text, which on a phone read as disabled even when it was not. */
.ot-filter-clear[disabled] { opacity: .45; cursor: default; }
/* A scrim behind the panel, so a click anywhere shuts it. Without one the
   panel stays open while you click the very rows it is filtering. */
.ot-filter-veil { position: fixed; inset: 0; z-index: 39; background: transparent; border: 0; }

/* What is on, shown outside the panel. A filter you cannot see from the list
   is a filter you forget you set, and then the list is wrong and you do not
   know why. */
.ot-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 14px; }
.ot-chips-lab { font-size: 12px; font-weight: 700; color: var(--ot-faint); }
.ot-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 7px 5px 11px; border-radius: 999px;
  background: var(--ot-surface); border: 1px solid var(--ot-border);
  font-size: 12px; font-weight: 700; color: var(--ot-text);
}
.ot-chip button {
  display: grid; place-items: center; width: 17px; height: 17px;
  border: 0; border-radius: 999px; background: transparent;
  color: var(--ot-faint); cursor: pointer; padding: 0;
}
.ot-chip button:hover { background: var(--ot-surface-2); color: var(--ot-text); }
.ot-chip svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-width: 2.5;
  stroke-linecap: round; display: block; }
.ot-chips-all {
  background: transparent; border: 0; padding: 0 0 0 2px; cursor: pointer;
  font-family: inherit; font-size: 12px; font-weight: 700; color: var(--ot-link);
}
.ot-chips-all:hover { text-decoration: underline; }

@media (max-width: 720px) {
  /* The head stacks on a phone, so the controls get the full width and the
     search takes what the tools beside it leave. */
  .ot-head-tools { justify-content: flex-start; }
  .ot-search { flex: 1 1 130px; width: auto; }
  /* The picker takes the first line to itself, and the tools share the next.
     It sizes to the event it is showing, so without this the row wrapped
     wherever that name happened to end: a short title left search and export
     beside it, a long one pushed export onto a line of its own with nothing
     next to it. Which line things land on should not depend on what the event
     is called. */
  /* min-width:0, or a select is as wide as its longest option: a long event
     name pushed the whole page past the right edge of the phone. */
  .ot-head-select { flex: 1 1 100%; max-width: 100%; min-width: 0; width: 100%; }
  /* The word goes and the glyph stays, like every other tool on the row. At
     390 the head is search, Filter, select and export: with the word, Filter
     is 118px and the four of them come to exactly the width of the row, so
     export wrapped to a line of its own. The count badge still says whether
     anything is on, which is the part the word was not carrying. */
  .ot-filter-word { display: none; }
  /* Anchored to the head rather than the button: 290px hanging off a button
     near the right edge of a 390px screen runs off the side. .ot-page-head is
     position:relative above, so static here means the panel spans the head -
     not that it climbs to whichever ancestor happens to be positioned. */
  .ot-filter { position: static; }

  /* A sheet, not a dropdown that got big.
   *
   * Anchored under the head it was neither: it started below the search row,
   * ran most of the way down the screen and left a sliver of list showing
   * underneath, so it read as a panel that had outgrown its button. Every
   * other thing this size on a phone is a sheet from the bottom - the event
   * panel, the buy sheet - and this has the same shape as one already: a
   * scrolling body with the two actions pinned along its foot.
   *
   * Bottom-anchored also puts the rows and the Show button in the half of
   * the screen a thumb reaches, which is where a list of taps belongs. */
  .ot-filter-panel {
    position: fixed; inset: auto 0 0 0; width: auto; top: auto;
    max-height: min(80vh, 86dvh);
    border-radius: 18px 18px 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
    /* Clear of the shell's tab bar and the home indicator: the sheet runs to
       the bottom of the screen and the bar paints over its last 67px, so the
       padding is what keeps the rows and the two buttons out from under it.
       The fallback is what this panel is worth on a page with no bar.
       The foot is sticky bottom:0 and needs no offset of its own - a scroll
       container's sticky bottom is measured from its content edge, so naming
       the bar's height again parked the buttons 67px above the bar instead of
       on it, with a band of empty panel underneath. */
    padding-bottom: var(--ot-tabbar-h, env(safe-area-inset-bottom, 0px));
    box-shadow: 0 -12px 40px rgba(4, 8, 20, .22);
  }
  /* And the veil dims. Transparent is right for a dropdown you dismiss by
     clicking past it; a sheet covering most of the screen needs the page
     behind it to look covered. */
  .ot-filter-veil { background: var(--ot-scrim); }

  /* As a sheet it follows the pop-up rules (DESIGN-SYSTEM.md, Pop-ups):
     a title and a close at the head, groups that are an eyebrow and nothing
     else - no rule drawn between them, the air does that (rule 10) - and a
     foot of two buttons side by side (rules 11 and 13). The shell's header
     and tab bar step aside while it is up, so there is no bar to clear. */
  .ot-filter-panel {
    z-index: 10041;
    padding-bottom: 0;
    border-radius: var(--ot-r-xl, 20px) var(--ot-r-xl, 20px) 0 0;
    animation: ot-filter-up .26s cubic-bezier(.22, 1, .36, 1) both;
  }
  .ot-filter-veil { z-index: 10040; }
  .ot-filter-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    position: sticky; top: 0; z-index: 1;
    padding: var(--ui-3, 12px) var(--ui-2, 8px) 0 var(--ui-4, 16px); background: var(--ot-surface);
  }
  .ot-filter-title { margin: 0; font-size: 18px; font-weight: 700; color: var(--ot-text); }
  .ot-filter-x {
    display: grid; place-items: center; width: 44px; height: 44px; flex: none;
    border: 0; border-radius: 999px; background: transparent; color: var(--ot-muted); cursor: pointer;
  }
  .ot-filter-x:active { background: var(--ot-surface-2); }
  /* The design system's spacing (ot-ui.css): the 16px gutter, a 12px gap
     between groups, the eyebrow 4px over its rows, and rows of 44px - still
     a thumb's height, without the air that made a short list a long scroll. */
  .ot-filter-group { padding-bottom: 0; }
  .ot-filter-group::after { content: none; }
  .ot-filter-legend {
    padding: var(--ui-3, 12px) var(--ui-4, 16px) var(--ui-1, 4px);
    font-weight: 600; letter-spacing: .09em;
  }
  /* border-box, so 44 is the whole row and not 44 plus the padding. */
  .ot-filter-row { box-sizing: border-box; min-height: 44px; padding: var(--ui-1, 4px) var(--ui-4, 16px); font-size: var(--ui-text-base, 16px); font-weight: 500; }
  .ot-filter-meta { font-size: var(--ui-text-sm, 14px); }
  .ot-filter-foot {
    gap: var(--ui-3, 12px); padding: var(--ui-3, 12px) var(--ui-4, 16px) calc(var(--ui-3, 12px) + env(safe-area-inset-bottom, 0px));
    margin-top: var(--ui-2, 8px);
  }
  .ot-filter-foot .ui-btn { flex: 1 1 0; min-width: 0; min-height: 44px; font-size: var(--ui-text-base, 16px); }
  .ot-filter-clear[disabled] { opacity: .5; }
  @keyframes ot-filter-up { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
  @media (prefers-reduced-motion: reduce) { .ot-filter-panel { animation: none; } }
}
