/* =============================================================
   WowCodes Portal — portal.css
   Design tokens mirror shared.css (wowcodes.in)
   ============================================================= */

/* ── Reset / Base ──────────────────────────────────────────── */
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'DM Sans', sans-serif;
  line-height: 1.5;
}

a {
  color: inherit;
  text-decoration: none;
}

/* ── Scrollbar ─────────────────────────────────────────────── */
::-webkit-scrollbar {
  width: 4px;
  height: 4px;
}
::-webkit-scrollbar-track {
  background: var(--bg2);
}
::-webkit-scrollbar-thumb {
  background: var(--bg4);
  border-radius: 2px;
}

/* =============================================================
   LAYOUT
   ============================================================= */

.portal-layout {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.topbar-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--amber-bg2);
  color: var(--amber-d);
  font-size: 11px;
  font-weight: 700;
  font-family: 'DM Sans', sans-serif;
  flex-shrink: 0;
}

/* The avatar renders on .pt-top's dark strip, where --amber is the
   light-ground value and only clears 2.93:1. */
.pt-top .topbar-avatar {
  color: var(--amber-l);
}

/* Mobile menu button — hidden by default, animates to a close (X) via
   aria-expanded, mirroring the hamburger on wowcodes.in's main nav. It only
   ever renders inside .pt-top, so it's styled for that surface directly
   rather than for a light ground it never sits on. */
.mobile-menu-btn {
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 36px;
  height: 36px;
  gap: 5px;
  padding: 0;
  background: transparent;
  border: 0.5px solid var(--on-dark-35);
  border-radius: var(--r);
  cursor: pointer;
  flex-shrink: 0;
  transition: background 0.15s;
}
.mobile-menu-btn:hover {
  background: var(--on-dark-10);
}
.mobile-menu-btn span {
  display: block;
  width: 16px;
  height: 1.5px;
  background: var(--on-dark);
  border-radius: 2px;
  transition: transform 0.2s, opacity 0.2s, width 0.2s;
}
.mobile-menu-btn[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.mobile-menu-btn[aria-expanded="true"] span:nth-child(2) { opacity: 0; width: 0; }
.mobile-menu-btn[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* ── Body / Sidebar / Main ─────────────────────────────────── */
.portal-body {
  display: flex;
  flex-direction: row;
  height: calc(100vh - 113px); /* .pt-top (60) + .pt-nav (53) — measured, not the old one-row header */
}

.portal-sidebar {
  width: 220px;
  flex-shrink: 0;
  background: var(--bg2);
  border-inline-end: 0.5px solid var(--border);
  overflow-y: auto;
  padding: 16px 0;
}

.portal-main {
  flex: 1;
  overflow-y: auto;
  /* The padding is a custom property because .pt-foot below has to cancel it
     exactly. Written as two literals, the pair silently disagreed the moment a
     media query changed one of them — see that rule's own comment. */
  --portal-main-pad: 32px;
  padding: var(--portal-main-pad);
  background: var(--bg);
  /* A column so the footer inside it can be pushed to the bottom. On a short
     page that puts the footer at the foot of the viewport instead of a screen
     below it; on a long one it scrolls after the content as usual. */
  display: flex;
  flex-direction: column;
}
.portal-main > .pt-foot {
  margin-block-start: auto;
  /* Back out the column's own padding so the footer spans the full width, the
     way it does on the shop.
     TRACKS THE PADDING, never repeats its value. As a literal -32px this went
     wrong the moment the phone breakpoint dropped .portal-main to 16px: the
     footer then hung 16px past each edge, and because it is the widest thing
     on the page that gave EVERY dashboard and checkout page 64px of sideways
     scroll at 390px — the whole layout looking shoved left and clipped on the
     right, with nothing on screen to explain it. */
  margin-inline: calc(var(--portal-main-pad) * -1);
  margin-block-end: calc(var(--portal-main-pad) * -1);
}

.pt-foot-gap {
  flex: none;
  height: 48px;
}

.sidebar-scrim {
  display: none;
}

/* =============================================================
   SIDEBAR NAV
   ============================================================= */

.sidebar-nav {
  list-style: none;
  padding: 0 12px;
  margin: 0;
}

.sidebar-section-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text3);
  padding: 16px 8px 6px;
  font-family: 'DM Sans', sans-serif;
  font-weight: 600;
}
.sidebar-section-label:first-child {
  padding-top: 8px;
}

.sidebar-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  margin: 1px 0;
  border-radius: var(--r);
  font-size: 13px;
  color: var(--text2);
  text-decoration: none;
  transition: color 0.15s, background 0.15s;
}
.sidebar-link:hover {
  color: var(--text);
  background: var(--bg3);
}
.sidebar-link.active {
  color: var(--base-d);
  background: var(--base-bg);
  font-weight: 600;
}
.sidebar-link.active svg {
  color: var(--base-d);
}
.sidebar-link:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: -2px;
}

/* A count beside a link — conversations waiting on the customer (Help
   inbox). Pushed to the row's end so the labels above and below it keep one
   left edge. */
.sidebar-badge {
  margin-inline-start: auto;
  min-width: 20px;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--amber-bg2);
  color: var(--amber-d);
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

/* =============================================================
   SIDEBAR — mobile-only head/footer (drawer chrome)
   Hidden on desktop, where the sidebar is always-on and needs
   neither a logo/close row nor a duplicate account footer.
   ============================================================= */

.sidebar-mobile-head,
.sidebar-mobile-footer {
  display: none;
}

.sidebar-mobile-logo {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-size: 16px;
  color: var(--text);
  text-decoration: none;
}
.sidebar-mobile-logo img {
  height: 20px;
  width: auto;
  display: block;
}

.sidebar-close-btn {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--bg3);
  border: 0.5px solid var(--border2);
  color: var(--text2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}
.sidebar-close-btn:hover {
  color: var(--text);
  background: var(--bg4);
}

.sidebar-mobile-footer-name {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--text2);
  overflow-wrap: anywhere;
}

.sidebar-mobile-footer-logout {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--text3);
  transition: color 0.15s;
}
.sidebar-mobile-footer-logout:hover {
  color: var(--amber);
}

/* =============================================================
   BOTTOM NAV — fixed tab bar, mobile only
   ============================================================= */

.portal-bottomnav {
  display: none;
  background: var(--bg2);
  border-block-start: 1px solid var(--border);
}

.bn-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 6px 2px;
  border: none;
  background: none;
  color: var(--text3);
  font-size: 10px;
  font-family: 'DM Sans', sans-serif;
  cursor: pointer;
  border-radius: var(--r);
  transition: color 0.15s;
}
.bn-item svg {
  width: 19px;
  height: 19px;
}
.bn-item.active,
.bn-item[aria-expanded="true"] {
  color: var(--base);
}
.bn-item:active {
  transform: scale(0.95);
}

/* The icon's box, so a count can sit on its corner without moving the label. */
.bn-icon {
  position: relative;
  display: inline-flex;
}
.bn-badge {
  position: absolute;
  top: -6px;
  inset-inline-end: -10px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--amber);
  color: var(--on-amber);
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

/* =============================================================
   PAGE HEADER
   ============================================================= */

.page-header {
  margin-bottom: 28px;
}

.page-title {
  font-family: 'Fraunces', serif;
  font-size: 24px;
  color: var(--text);
  margin: 0;
  line-height: 1.2;
}

.page-subtitle {
  font-size: 13px;
  color: var(--text2);
  margin-top: 4px;
  margin-bottom: 0;
}

/* =============================================================
   SECTION TITLE
   ============================================================= */

.section-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text3);
  margin-bottom: 12px;
  margin-top: 0;
  font-family: 'DM Sans', sans-serif;
  font-weight: 600;
}

/* =============================================================
   CARDS
   ============================================================= */

.portal-card {
  background: var(--bg2);
  border: 0.5px solid var(--border);
  border-radius: var(--rl);
  padding: 24px;
  /* dashboard/index.php's activity table sits straight in a card with no
     overflow-x wrapper of its own — an unbreakable string (a long reference
     ID, say) would otherwise drag the whole main column into a sideways
     scroll instead of just this card. */
  overflow-x: auto;
  /* Load-bearing, and not a tidy-up. .portal-main is a flex column, and a flex
     item whose overflow is not `visible` loses its automatic minimum size — so
     the declaration above silently made this card shrinkable to nothing. Inside
     .portal-body's fixed height: calc(100vh - 113px) the column then squashed
     the card to fit the viewport and let it scroll INSIDE itself, which is a
     thing overflow-x was never asking for (setting one axis computes the other
     to auto). A tall form came out clipped mid-field with the footer directly
     under the cut, reading as the footer sitting on top of the page.
     flex-shrink: 0 keeps the card its content's height and lets .portal-main
     scroll instead, which is what its own overflow-y: auto is there for. */
  flex-shrink: 0;
}

/* Stat tiles — the dashboard's figures, each one a link to the page behind it.
   Proportions follow the CodePlus widget they are modelled on: a large tinted
   rounded-square icon, a border in the tile's own colour rather than the
   neutral one, a small radius, and the chevron pushed to the far edge instead
   of tucked beside the text. */
.stat-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  margin-block-end: 32px;
}

.stat-tile {
  --tile: var(--base);
  --tile-ink: var(--base-d);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 20px 18px;
  background: var(--bg2);
  border: 1px solid var(--tile);
  border-radius: 6px;
  box-shadow: var(--shadow-card);
  color: inherit;
  text-decoration: none;
  transition: box-shadow .15s ease, transform .15s ease;
}
.stat-tile:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-1px); }
.stat-tile:focus-visible { outline: 2px solid var(--tile-ink); outline-offset: 2px; }

.stat-tile.is-base   { --tile: var(--base);   --tile-ink: var(--base-d); }
.stat-tile.is-teal   { --tile: var(--teal);   --tile-ink: var(--teal-d); }
.stat-tile.is-amber  { --tile: var(--amber);  --tile-ink: var(--amber-d); }
.stat-tile.is-danger { --tile: var(--danger); --tile-ink: var(--danger-d); }

.stat-tile__lead { display: flex; align-items: center; gap: 16px; min-width: 0; }

/* A rounded square, not a circle, and large enough to anchor the row. The tint
   is the tile's colour at 20%; the glyph is the -d step, which clears 4.5:1
   against that wash rather than against the card. */
.stat-tile__icon {
  position: relative;
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--tile-ink);
}
.stat-tile__icon::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--tile);
  opacity: .2;
  border-radius: inherit;
  z-index: -1;
}
.stat-tile__icon svg { width: 24px; height: 24px; }

.stat-tile__body { min-width: 0; }
.stat-tile__label {
  display: block;
  margin: 0;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text2);
}
/* Wraps rather than clips: a long figure or a long translation stays readable
   and the tile gets taller. */
.stat-tile__value {
  display: block;
  margin: 2px 0 0;
  font-family: var(--display);
  font-size: 28px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--text);
  overflow-wrap: anywhere;
}
.stat-tile__note {
  display: block;
  margin: 2px 0 0;
  font-size: 12px;
  color: var(--text3);
  overflow-wrap: anywhere;
}
.stat-tile__arrow {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 30px;
  color: var(--text3);
}
.stat-tile:hover .stat-tile__arrow { color: var(--tile-ink); }

/* =============================================================
   MEMBER CARD — membership.php's hero. A loyalty-card treatment of
   data the page already had: the subscription cards below carry the
   status/cancel machinery, this carries the "what tier am I, what
   have I got" summary — current tier, Store credit balance, this
   tier's perks, and a level-up CTA to the next one up.
   ============================================================= */

.member-card {
  position: relative;
  overflow: hidden;
  border-radius: var(--rl);
  padding: 28px;
  margin-bottom: 8px;
  background: linear-gradient(135deg, #241a08 0%, var(--topbar-bg) 60%, var(--topbar-bg2) 100%);
  color: var(--on-dark);
  box-shadow: var(--shadow-card);
  /* .portal-main is a column flex container (see its own rule), and a flex
     item whose overflow is not `visible` loses its automatic minimum size —
     the exact gotcha .portal-card's own flex-shrink: 0 already documents.
     Missing it here let the card get crushed toward zero height on any
     viewport short enough that this page's total content didn't fit
     .portal-main's available space, hiding everything below the tier badge
     while every sibling section below it (no overflow:hidden of its own,
     so no min-height:auto to lose) kept its normal size regardless. */
  flex-shrink: 0;
}
.member-card__glow {
  position: absolute;
  top: -70px;
  inset-inline-end: -70px;
  width: 240px;
  height: 240px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--amber-rgb), .35) 0%, transparent 72%);
  pointer-events: none;
}

.member-card__top {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}
.member-card__id { min-width: 0; }
.member-card__tier {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: 999px;
  background: var(--amber-25);
  border: 0.5px solid rgba(var(--amber-rgb), .4);
  color: var(--amber-l);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
}
.member-card__tier--none { background: var(--on-dark-10); border-color: var(--on-dark-14); color: var(--on-dark-70); }
.member-card__name {
  font-family: var(--display);
  font-size: 21px;
  margin: 14px 0 2px;
  overflow-wrap: anywhere;
}
.member-card__since { font-size: 12px; color: var(--on-dark-70); margin: 0; }
.member-card__mark {
  flex: 0 0 auto;
  color: rgba(var(--amber-rgb), .5);
}

.member-card__stats {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-top: 22px;
}
.member-card__stat {
  background: var(--on-dark-06);
  border: 0.5px solid var(--on-dark-14);
  border-radius: var(--r);
  padding: 12px 14px;
}
.member-card__stat-label {
  display: block;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--on-dark-55);
}
.member-card__stat-value {
  display: block;
  font-family: var(--display);
  font-size: 19px;
  margin-top: 3px;
  overflow-wrap: anywhere;
}

.member-card__perks {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 18px;
}
.member-card__perk {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--on-dark-06);
  border: 0.5px solid var(--on-dark-14);
  color: var(--on-dark-70);
  font-size: 11.5px;
}
.member-card__perk svg { color: rgba(var(--teal-rgb), .95); flex: none; }

.member-card__levelup {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 20px;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--amber);
  color: var(--on-amber);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  transition: opacity .15s;
}
.member-card__levelup:hover { opacity: .88; }
.member-card__toptier {
  position: relative;
  margin: 20px 0 0;
  font-size: 12.5px;
  color: var(--on-dark-70);
}

/* =============================================================
   DOWNLOAD MENU — products.php's Download split button. Shares
   .pt-menu's card and the [data-chrome-menu] behaviour with the
   topbar's account menu (see chrome.css's own note by that
   selector): same shape, same script, no second implementation.
   ============================================================= */

.dl-menu { position: relative; display: inline-flex; }
.dl-menu__caret { transition: transform .15s ease; }
.dl-menu__btn[aria-expanded="true"] .dl-menu__caret { transform: rotate(180deg); }
/* .pt-menu defaults to right-anchored, sized for the topbar's account button
   sitting at the far right of the page. This trigger sits at the START of a
   card's action row instead, so a right anchor would swing the panel out
   past the card's (and on a narrow layout, the sidebar's) left edge. */
.dl-menu [data-chrome-menu-panel] { inset-inline-start: 0; inset-inline-end: auto; --menu-origin: left; }

/* .portal-card's overflow-x: auto computes overflow-y to auto too (see that
   rule's own comment) — a safety net for a long unbreakable string, which
   clips this menu's popover the moment it needs to render below the card's
   bottom edge instead. Scoped to only a card actually holding an open menu,
   so every other card keeps the safety net. */
.portal-card:has(.dl-menu.is-open) { overflow: visible; }

.pt-menu__link[aria-disabled="true"] {
  opacity: .45;
  cursor: default;
  pointer-events: none;
}

/* =============================================================
   PROMO BANNER — products.php's affiliate-link promo. What shows
   here (or whether anything does) is edited in the console's
   Affiliate links screen, not in this markup.
   ============================================================= */

.promo-banner {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  margin-bottom: 24px;
}
/* Icon + title/text as one unit that never splits — a narrow viewport wraps
   the CTA onto its own line below (the .promo-banner flex-wrap above), but
   the icon and copy stay side by side always (flex-wrap defaults to nowrap
   here), which is what actually shortens the card on mobile: without this
   the icon used to end up alone on its own line above the copy, the tallest
   possible arrangement of the same three pieces. */
.promo-banner__header {
  display: flex;
  align-items: center;
  gap: 20px;
  flex: 1 1 240px;
  min-width: 0;
}
.promo-banner__icon {
  position: relative;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--teal-d);
}
.promo-banner__icon::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--teal);
  opacity: .16;
  border-radius: inherit;
  z-index: -1;
}
.promo-banner__icon svg { width: 22px; height: 22px; }
.promo-banner__body { flex: 1 1 auto; min-width: 0; }
.promo-banner__title { font-family: 'Fraunces', serif; font-size: 16px; color: var(--text); margin: 0; }
.promo-banner__text { font-size: 13px; color: var(--text2); margin: 4px 0 0; }
.promo-banner__cta { flex: 0 0 auto; }

/* A featured placement (top of a page, not a footnote after other content) —
   a richer tinted-gradient card, a filled icon tile instead of a flat tint,
   and a primary-weight CTA, so it reads as something worth noticing rather
   than something to skim past. */
.promo-banner--featured {
  position: relative;
  overflow: hidden;
  background: linear-gradient(135deg, var(--teal-12), var(--bg2) 65%);
  border: 1px solid var(--teal-border);
  box-shadow: var(--shadow-card);
}
.promo-banner--featured .promo-banner__icon {
  width: 56px;
  height: 56px;
  border-radius: 14px;
  background: var(--teal);
  color: var(--on-teal);
  box-shadow: 0 0 0 6px var(--teal-12);
}
.promo-banner--featured .promo-banner__icon::after { content: none; }
.promo-banner--featured .promo-banner__icon svg { width: 26px; height: 26px; }
.promo-banner--featured .promo-banner__title { font-size: 18px; }
.promo-banner--featured .promo-banner__text { font-size: 14px; }

/* =============================================================
   PLAN CARDS — membership.php's subscription cards and pricing
   cards share this shape: a flex column so a price or a cancel
   button pins to the bottom of the card however tall its benefit
   list or description grows.
   ============================================================= */

.plan-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 16px;
  transition: box-shadow .15s ease, transform .15s ease;
}
.plan-card:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-1px); }

.plan-card__head {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.plan-card__head--split { justify-content: space-between; }
.plan-card__icon {
  position: relative;
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--tile-ink, var(--amber-d));
}
/* z-index 0 and not -1. .plan-card__icon is position:relative with z-index
   auto, so it opens no stacking context of its own — at -1 this tint painted
   behind .portal-card's own --bg2 fill and was invisible on every card that
   has ever rendered. The glyph is lifted over it instead of the tint being
   dropped, because an inset:0 ::after cannot sit under a sibling. */
.plan-card__icon::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--tile, var(--amber));
  opacity: .16;
  border-radius: inherit;
  z-index: 0;
}
.plan-card__icon svg { position: relative; z-index: 1; width: 19px; height: 19px; }
.plan-card__name {
  font-family: 'Fraunces', serif;
  font-size: 18px;
  color: var(--text);
  margin: 0;
}

.plan-card__period-bar {
  height: 6px;
  border-radius: 99px;
  background: var(--bg3);
  overflow: hidden;
}
.plan-card__period-fill {
  height: 100%;
  border-radius: inherit;
  transition: width .2s ease;
}
.plan-card__period-label {
  font-size: 12px;
  color: var(--text3);
  margin: 8px 0 0;
}

/* Products claimed free through this subscription (its shadow licence's
   'campaign' entitlements) — see dashboard/membership.php. */
.plan-card__claims-heading {
  font-family: 'Fraunces', serif;
  font-size: 13px;
  color: var(--text);
  margin: 16px 0 6px;
}
.plan-card__claims {
  margin: 0;
  padding-left: 18px;
  font-size: 12.5px;
  color: var(--text2);
  line-height: 1.7;
}

/* =============================================================
   AVAILABLE PLANS — the dashboard's half of the pricing cards.
   The cards themselves are assets/css/pricing.css, shared
   verbatim with the storefront; only what the dashboard adds
   over that component lives here — its heading row, its own
   button as the card's CTA, and the "Recommended" flag the
   storefront has no notion of.
   ============================================================= */

/* The storefront centres its heading and switch under a marketing headline.
   A dashboard section heading is start-aligned, so the switch rides the same
   row instead of floating centred under it, belonging to nothing. */
.plans-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin: 28px 0 14px;
  /* .portal-main is a flex column, and .pricing-toggle inside this row carries
     overflow-x: auto — a flex item whose overflow is not `visible` loses its
     automatic minimum size, which crushed the switch to a 10px sliver with its
     buttons hanging out of it. The same gotcha .portal-card and .member-card
     each already document on themselves. */
  flex-shrink: 0;
}
.plans-heading {
  font-family: var(--display);
  font-size: 18px;
  color: var(--text);
  margin: 0;
  scroll-margin-top: 24px;
}
/* The shared component centres itself under the storefront's headline; on the
   heading row it sits at the end instead. */
.plans-header .pricing-toggle { margin: 0; }

/* The saving hint on the cadence switch. The dashboard's one addition to the
   shared toggle: the per-card tabs this replaced carried it, and without it
   nothing on the row says why a buyer would look at yearly. */
.pricing-toggle__save {
  margin-inline-start: 7px;
  font-size: .75rem;
  font-weight: 700;
  color: var(--teal-d);
}

/* The storefront's .shop-cta is a block, so the shared sheet only pins the
   button to the foot of the card. .btn-primary/.btn-secondary are inline-flex
   and would otherwise shrink to their label. */
.btn-primary.pricing-card__cta,
.btn-secondary.pricing-card__cta {
  width: 100%;
  justify-content: center;
}
/* Rendered as a <span>, not a link — "Subscribed" and "Full" are states, not
   actions, and neither is reachable by keyboard or pointer. */
.pricing-card__cta.is-inert { cursor: default; opacity: .6; }

/* The tier the hero card's level-up CTA points at, called out so the two
   halves of the page read as one recommendation rather than the hero
   suggesting one thing and the grid saying nothing about it. The storefront
   sells to visitors with no membership to compare against and has no
   equivalent, so this is the dashboard's own. */
.pricing-card.is-recommended {
  border-color: var(--amber);
  box-shadow: var(--shadow-card-hover);
}
.pricing-card__flag {
  position: absolute;
  top: 14px;
  inset-inline-end: 14px;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--amber);
  color: var(--on-amber);
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
}

/* =============================================================
   ACTIVITY ROWS — dashboard/index.php's Recent Activity. A plain
   table put date, description, type and amount in four columns that
   a phone-width viewport had no room for; each badge pair alone was
   nearly as wide as the column. This reads as one line per
   transaction at desktop widths and wraps to two — description above,
   date/badges/amount below — without ever pushing content past the
   edge of the card.
   ============================================================= */

.activity-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 0.5px solid var(--border);
}
.activity-row:last-child { border-bottom: none; }

.activity-row__left { min-width: 0; }
.activity-row__desc {
  display: block;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text);
  overflow-wrap: anywhere;
}
.activity-row__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}
.activity-row__date {
  font-size: 11.5px;
  color: var(--text3);
  white-space: nowrap;
}

.activity-row__right {
  flex: 0 0 auto;
  text-align: end;
}
.activity-row__amount {
  display: block;
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
  font-family: var(--display);
}

/* =============================================================
   HOME — dashboard/index.php. "Needs your attention" first: one row per
   thing whose next move is the customer's, each with a single action.
   Then the product list beside balances and recent activity, which stack
   under it on a phone.
   ============================================================= */

.attn-card { padding: 0; margin-block-end: 28px; }
.attn-card__head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 16px 20px;
  border-block-end: 0.5px solid var(--border);
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}
.attn-card__count {
  min-width: 22px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--bg4);
  font-size: 12px;
  font-weight: 600;
  text-align: center;
}
.attn-card__clear {
  margin: 0;
  padding: 22px 20px;
  font-size: 13.5px;
  color: var(--text2);
}
.attn-list { list-style: none; margin: 0; padding: 0; }
.attn-item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px;
  padding: 14px 20px;
}
.attn-item + .attn-item { border-block-start: 0.5px solid var(--border); }
.attn-item__icon {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--base-bg);
  color: var(--base-d);
}
.attn-item__icon.is-amber { background: var(--amber-bg2); color: var(--amber-d); }
.attn-item__icon.is-teal { background: var(--teal-bg); color: var(--teal-d); }
.attn-item__icon svg { width: 18px; height: 18px; }
.attn-item__body {
  flex: 1 1 220px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.attn-item__title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  overflow-wrap: anywhere;
}
.attn-item__detail { font-size: 12.5px; color: var(--text2); }
.attn-item__action { flex: 0 0 auto; }

.home-split {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: 24px;
  align-items: start;
}
.home-split__side {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.home-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.home-section-head__link { font-size: 12.5px; font-weight: 600; }
.home-split__side .home-section-head { margin-block-start: 12px; }

.home-products { padding: 4px 20px; }
.home-products__list { list-style: none; margin: 0; padding: 0; }
.home-product {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 0;
}
.home-product + .home-product { border-block-start: 0.5px solid var(--border); }
.home-product__icon {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--base-bg);
  color: var(--base-d);
}
.home-product__icon svg { width: 18px; height: 18px; }
.home-product__body {
  flex: 1 1 180px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  color: inherit;
  text-decoration: none;
}
.home-product__body:hover .home-product__name { color: var(--base-d); }
.home-product__name { font-size: 14px; font-weight: 600; color: var(--text); }
.home-product__type { font-weight: 500; color: var(--text2); font-size: 13px; }
.home-product__name .acq-badge { margin-inline: 6px 0; vertical-align: 1px; }
.home-product__site { font-size: 12.5px; color: var(--text2); overflow-wrap: anywhere; }
.home-product__pills {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.home-balances {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.home-balances .stat-tile { padding: 14px 16px; }
.home-balances__wide { grid-column: 1 / -1; }
.home-activity { padding: 4px 20px; }
.activity-row__amount.is-credit { color: var(--teal); }
.activity-row__amount.is-debit { color: var(--danger); }

@media (max-width: 900px) {
  .home-split { grid-template-columns: minmax(0, 1fr); }
}

/* =============================================================
   RENEW SUPPORT — dashboard/renew-support.php. The membership page's
   pricing cards, with one button per length instead of one price.
   ============================================================= */

.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-block-end: 12px;
  font-size: 12.5px;
  color: var(--text3);
}
.crumbs a { color: var(--text2); text-decoration: none; }
.crumbs a:hover { color: var(--text); }
.renew-note { margin: -12px 0 20px; font-size: 13px; color: var(--text2); }
.renew-options {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-block-start: 16px;
}
.renew-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 44px;
  padding: 12px 14px;
  border: 1px solid var(--border2);
  border-radius: var(--r);
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.15s, background 0.15s;
}
.renew-option:hover { border-color: var(--amber); background: var(--amber-bg); }
.renew-option:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.renew-option__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.renew-option__months { font-size: 14px; font-weight: 600; }
.renew-option__until { font-size: 12px; color: var(--text2); }
.renew-option__price { font-family: var(--display); font-size: 17px; font-weight: 700; white-space: nowrap; }
@media (max-width: 768px) {
  .attn-item { padding: 14px 16px; }
  .attn-item__action { flex: 1 1 100%; justify-content: center; }
  .home-product__pills { padding-inline-start: 50px; }
  .home-products, .home-activity { padding: 4px 16px; }
}

/* =============================================================
   LICENCES — dashboard/products.php's cards and each licence's own
   page, dashboard/licence.php (/dashboard/products/<ref>).
   ============================================================= */

.lic-list { display: flex; flex-direction: column; gap: 14px; }
/* overflow: visible for the Download menu's panel — see .portal-product-section's rule. */
.lic-card { padding: 18px 20px; overflow: visible; }
.lic-card__top { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 10px; }
.lic-card__title {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  color: var(--text);
  text-decoration: none;
  min-width: 0;
}
.lic-card__title:hover .lic-card__name { color: var(--base-d); }
.lic-card__name { font-family: 'Fraunces', serif; font-size: 18px; font-weight: 600; overflow-wrap: anywhere; }
.lic-card__type { font-size: 13px; color: var(--text2); }
.lic-card__top .acq-badge { margin: 0; }
.lic-card__meta { margin: 4px 0 12px; font-size: 12.5px; color: var(--text3); }
.lic-card__note { margin: 0 0 14px; font-size: 14px; color: var(--text2); }
.lic-card__pills { display: flex; flex-wrap: wrap; gap: 8px; margin-block-end: 14px; }
.lic-card__chip { text-decoration: none; }
.lic-card__chip:hover { filter: brightness(0.96); }
.lic-card__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.lic-card__manage { margin-inline-start: auto; display: inline-flex; align-items: center; gap: 4px; }

.lic-head { padding: 22px 24px; margin-block-end: 18px; }
.lic-head__main { display: flex; align-items: center; gap: 14px; }
.lic-head__icon {
  flex: 0 0 auto;
  width: 46px;
  height: 46px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--base-bg);
  color: var(--base-d);
}
.lic-head__icon svg { width: 22px; height: 22px; }
.lic-head__titles { min-width: 0; }
.lic-head__name {
  font-family: 'Fraunces', serif;
  font-size: 24px;
  line-height: 1.2;
  color: var(--text);
  margin: 0;
  overflow-wrap: anywhere;
}
.lic-head__sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--text2);
}
.lic-head__sub .acq-badge { margin: 0; }
.lic-facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  margin: 18px 0 0;
  border: 0.5px solid var(--border);
  border-radius: var(--r);
  overflow: hidden;
}
.lic-fact { padding: 12px 14px; min-width: 0; }
.lic-fact + .lic-fact { border-inline-start: 0.5px solid var(--border); }
.lic-fact dt {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text3);
  margin: 0 0 4px;
}
.lic-fact dd { margin: 0; font-size: 13.5px; color: var(--text); overflow-wrap: anywhere; }
.lic-fact__more, .lic-fact__muted { color: var(--text3); font-size: 12.5px; }
.lic-ref { font-family: 'DM Mono', monospace; font-size: 12.5px; }
.lic-head__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 18px; }

.lic-tabs {
  /* overflow-x lets the row scroll on a phone, and an overflowing flex item
     may otherwise shrink to nothing in .portal-main's column. */
  flex: 0 0 auto;
  display: flex;
  gap: 4px;
  margin-block-end: 16px;
  border-block-end: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}
.lic-tabs::-webkit-scrollbar { display: none; }
.lic-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  min-height: 44px;
  padding: 0 14px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text2);
  text-decoration: none;
  white-space: nowrap;
  border-block-end: 2px solid transparent;
  margin-block-end: -1px;
}
.lic-tab:hover { color: var(--text); }
.lic-tab.is-active { color: var(--text); border-block-end-color: var(--amber); }
.lic-tab__count {
  min-width: 18px;
  padding: 0 5px;
  border-radius: 99px;
  background: var(--bg4);
  color: var(--text2);
  font-size: 11px;
  line-height: 18px;
  text-align: center;
}
.lic-tab__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--amber); }

.lic-panel { margin-block-end: 16px; }
.lic-panel__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.lic-panel__title { font-family: 'Fraunces', serif; font-size: 17px; color: var(--text); margin: 0 0 6px; }
.lic-panel__head .lic-panel__title { margin: 0; }
.lic-panel__text { font-size: 13.5px; color: var(--text2); margin: 0 0 14px; }
.lic-panel__text:last-child { margin-block-end: 0; }
.lic-rows { list-style: none; margin: 6px 0 0; padding: 0; }
.lic-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 48px;
  padding: 10px 0;
}
.lic-row + .lic-row { border-block-start: 0.5px solid var(--border); }
.lic-row__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; font-size: 14px; color: var(--text); overflow-wrap: anywhere; }
.lic-row__meta { font-size: 12.5px; color: var(--text3); white-space: nowrap; }
.lic-row--wrap { align-items: flex-start; }
.lic-row__meta--wrap { white-space: normal; }
.lic-row__actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; }
.lic-row__price { font-weight: 700; font-size: 14px; color: var(--text); white-space: nowrap; }
.lic-panel__label {
  margin: 16px 0 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text3);
}
.lic-included > summary { cursor: pointer; list-style-position: inside; }
.lic-included[open] > summary { margin-block-end: 10px; }
.lic-included__list {
  margin: 0;
  padding-inline-start: 18px;
  columns: 2;
  column-gap: 28px;
  font-size: 13px;
  line-height: 1.8;
  color: var(--text2);
}
.lic-included--inline > summary { font-size: 12.5px; color: var(--text2); }
.lic-included--inline .lic-included__list { columns: 1; margin-block-start: 6px; }
.lic-releases { list-style: none; margin: 8px 0 0; padding: 0; }
.lic-release { padding: 14px 0; }
.lic-release + .lic-release { border-block-start: 0.5px solid var(--border); }
.lic-release__head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: 0 0 6px; font-size: 14px; color: var(--text); }
.lic-release__date { font-size: 12.5px; color: var(--text3); }
.lic-release__cat { margin: 8px 0 2px; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text3); }
.lic-release ul { margin: 0; padding-inline-start: 18px; font-size: 13px; line-height: 1.7; color: var(--text2); }
.lic-release__prose { margin: 0; font-size: 13px; line-height: 1.7; color: var(--text2); }

@media (max-width: 768px) {
  .lic-card { padding: 16px; }
  .lic-card__actions > * { flex: 1 1 auto; justify-content: center; }
  .lic-card__manage { margin-inline-start: 0; }
  .lic-card__actions .dl-menu__btn { flex: 1 1 auto; justify-content: center; }
  .lic-head { padding: 18px 16px; }
  .lic-head__name { font-size: 21px; }
  .lic-facts { grid-template-columns: minmax(0, 1fr); }
  .lic-fact {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px;
  }
  .lic-fact + .lic-fact { border-inline-start: 0; border-block-start: 0.5px solid var(--border); }
  .lic-fact dt { margin: 0; flex: 0 0 auto; }
  .lic-fact dd { text-align: end; }
  .lic-head__actions > * { flex: 1 1 auto; justify-content: center; }
  .lic-included__list { columns: 1; }
  .lic-row { flex-wrap: wrap; }
  .lic-row__actions { flex: 1 1 100%; justify-content: flex-start; }
}

/* MOVE A DOMAIN — dashboard/move-domain.php. */
.move-card { max-width: 640px; }
.move-note {
  margin: 0 0 16px;
  padding: 10px 12px;
  border-radius: var(--r);
  font-size: 13.5px;
}
.move-note--free { background: var(--teal-bg); color: var(--teal-d); }
.move-note--wait { background: var(--amber-bg); color: var(--amber-d); }
.move-form .form-input { width: 100%; }
.move-facts {
  margin: 16px 0 0;
  padding-inline-start: 18px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--text2);
}
.move-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 20px; }
@media (max-width: 768px) {
  .move-actions > * { flex: 1 1 auto; justify-content: center; text-align: center; }
}

/* Kept for partner/hub.php, which still uses it. */
.stat-card {
  background: var(--bg2);
  border: 0.5px solid var(--border);
  border-radius: var(--rl);
  padding: 24px;
}

.stat-value {
  font-family: 'Fraunces', serif;
  font-size: 28px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.1;
}

.stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text3);
  margin-top: 4px;
}

/* =============================================================
   GRIDS
   ============================================================= */

.portal-grid-2 {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}

.portal-grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

.portal-grid-4 {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}

/* A two-panel grid. Stretches to equal height by default — settings.php's
   Profile and Marketing Email cards read as one row of equal-height
   siblings on this alone. -top and -tight are the two modifiers this shape
   actually needs, applied alongside the base class rather than as separate
   full rules. */
.split-panel {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 24px;
}

/* An info column next to a form, unequal heights, so it stays top-aligned
   instead of stretching to match. */
.split-panel-top {
  align-items: start;
}

/* A pair of short fields (name/email, coupon/referral) rather than a
   sidebar-and-form split — tighter gap to match. */
.split-panel-tight {
  gap: 12px;
}

/* Title-row icon buttons on dashboard/products.php's cards — view-product
   -page and view-entitlement-details. Small and muted so they read as
   metadata affordances beside the h3, not another call to action competing
   with Download. */
.portal-card__icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  color: var(--text3);
  background: transparent;
  border: none;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  text-decoration: none;
}
.portal-card__icon-link:hover,
.portal-card__icon-link:focus-visible {
  background: var(--bg3);
  color: var(--text);
}

/* =============================================================
   ADD-ON CAROUSEL — dashboard/products.php nests a licence's separately
   -bought add-ons in a horizontal, scroll-snapping strip below its own
   full-width card. Each card's width is deliberately a fraction that does
   NOT divide the strip evenly (2.5 / 3.5 / 4.5 cards per viewport) so the
   next card is always sliced off at the edge — that sliver is what tells a
   customer there's more to scroll to, without a nav arrow or a dot
   indicator to build and keep in sync with scroll position. Scrollbar
   hidden (scrollbar-width plus the WebKit pseudo-element): the sliver
   already signals "more", so a scrollbar underneath is only noise.
   ============================================================= */

/* .portal-card's own overflow-x:auto (computing overflow-y to auto too —
   see that rule's comment) turns a card into a scroll container whose
   scrollHeight measures EVERY descendant's laid-out box, including one that
   is only invisible. A card's .dl-menu dropdown is hidden that way on
   purpose — visibility/opacity rather than display:none, so its open/close
   transition has something to animate (chrome.css's own comment on
   .pt-menu) — so the CLOSED panel still sits, full-size, well below the
   card's visible content. In the old two-column grid a taller neighbour's
   height (CSS Grid's default row-stretch) always covered that phantom
   panel's extent, so it never surfaced. A full-width card with no such
   neighbour shrinks to fit only its visible content, which is shorter than
   the phantom panel — turning it into a real internal scrollbar that can
   scroll the card's own title out of view above the fold. Neither this
   page's cards nor the carousel's hold an unbreakable long string that
   needs the horizontal-scroll protection .portal-card's rule exists for
   (a purchase key lives in its own readonly <input>) — overflow:visible
   removes the clipping/scroll context entirely, the same fix
   .portal-card:has(.dl-menu.is-open) already applies while a menu is
   actually open, made permanent here since these cards never need it closed
   either. */
.portal-product-section .portal-card,
.portal-addon-carousel__card {
  overflow: visible;
}

/* How a licence was acquired (wc_license_acquisitions()) — a distinct colour
   for paid vs free/coupon, so a claimed licence never reads as a purchase. */
.acq-badge { display: inline-block; font-size: 11.5px; font-weight: 600; line-height: 1; padding: 4px 8px; border-radius: 999px; background: var(--bg3); color: var(--text2); margin-inline-end: 6px; }
.acq-badge.is-paid { background: var(--base-bg); color: var(--base-d); }
.acq-badge.is-free_campaign, .acq-badge.is-free_checkout { background: var(--teal-bg); color: var(--teal-d); }
.acq-badge.is-coupon { background: var(--amber-bg); color: var(--amber-d); }

.portal-addon-carousel {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: 2px; /* keeps each card's own focus ring from being clipped */
  margin: 0 -2px;
}
.portal-addon-carousel::-webkit-scrollbar {
  display: none;
}

.portal-addon-carousel__card {
  flex: 0 0 auto;
  scroll-snap-align: start;
  width: 42%; /* ~2.5 cards visible on a phone */
}

@media (min-width: 768px) {
  .portal-addon-carousel__card {
    width: 29%; /* ~3.5 cards visible */
  }
}

@media (min-width: 1200px) {
  .portal-addon-carousel__card {
    width: 22%; /* ~4.5 cards visible */
  }
}

/* =============================================================
   ADD-ONS PAGE — dashboard/addons.php. One .addon-licence per licence:
   a header card naming the product, then a grid of what can still be
   added and the products page's carousel for what is already held.
   ============================================================= */

.addon-licence { margin-bottom: 36px; }

.addon-licence__head {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 18px;
  background: linear-gradient(135deg, var(--amber-bg), var(--bg2) 60%);
  box-shadow: var(--shadow-card);
}
.addon-licence__icon {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--amber);
  color: var(--on-amber);
}
.addon-licence__icon svg { width: 24px; height: 24px; }
.addon-licence__body { flex: 1 1 220px; min-width: 0; }
.addon-licence__name {
  font-family: 'Fraunces', serif;
  font-size: 20px;
  font-weight: 500;
  color: var(--text);
  margin: 0 0 6px;
}
.addon-licence__meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0;
}
.addon-licence__key {
  font-family: 'DM Mono', monospace;
  font-size: 12px;
  color: var(--text3);
  overflow-wrap: anywhere;
}
.addon-licence__counts { display: flex; gap: 8px; flex-wrap: wrap; }
.addon-count {
  padding: 6px 12px;
  border-radius: 99px;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
}
.addon-count--available { background: var(--amber-bg2); color: var(--amber-d); }
.addon-count--included  { background: var(--teal-12); color: var(--teal-d); }

.addon-licence__label { margin: 4px 0 12px; }
.addon-licence__empty {
  font-size: 13px;
  color: var(--text3);
  margin: 0;
  padding: 16px 20px;
  border: 1px dashed var(--border);
  border-radius: var(--rl);
}

.addon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 16px;
  margin-bottom: 20px;
}

.addon-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  overflow: visible;
  transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
}
.addon-card:hover {
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-1px);
  border-color: var(--amber-25);
}
.addon-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}
.addon-card__icon {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--amber-bg);
  color: var(--amber-d);
}
.addon-card__icon.is-theme { background: var(--base-bg); color: var(--base-d); }
.addon-card__icon.is-owned { background: var(--teal-12); color: var(--teal-d); }
.addon-card__icon svg { width: 20px; height: 20px; }
.addon-card__name {
  font-family: 'Fraunces', serif;
  font-size: 16px;
  font-weight: 500;
  color: var(--text);
  margin: 0;
}
.addon-card__tagline {
  font-size: 13px;
  line-height: 1.55;
  color: var(--text2);
  margin: 0;
}
/* margin-top:auto pins price + buttons to the bottom however long the
   tagline runs, so a row of cards lines its buttons up. */
.addon-card__foot {
  margin-top: auto;
  padding-top: 14px;
  border-top: 0.5px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* Know more and Buy, side by side at equal width. */
.addon-card__actions { display: flex; gap: 8px; }
.addon-card__actions > * { flex: 1 1 0; justify-content: center; }
.addon-card__price {
  font-family: 'Fraunces', serif;
  font-size: 20px;
  color: var(--text);
}

.addon-owned { display: flex; flex-direction: column; gap: 8px; }
.addon-owned__cta { margin-top: auto; align-self: flex-start; }

/* Know more — one per add-on, opened from any card offering it. */
.addon-info { max-width: 560px; }
.addon-info__head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 16px; }
.addon-info__head .badge { margin-left: auto; flex-shrink: 0; }
.addon-info__title { min-width: 0; }
.addon-info__title h3 { font-family: 'Fraunces', serif; font-size: 20px; font-weight: 500; color: var(--text); margin: 0; }
.addon-info__title p { font-size: 13px; color: var(--text2); margin: 4px 0 0; }
.addon-info__desc {
  font-size: 14px;
  line-height: 1.65;
  color: var(--text2);
  margin: 0;
  white-space: pre-line;
}
.addon-info__label { margin: 0 0 8px; }
.addon-info__parts {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 300px;
  overflow-y: auto;
}
.addon-info__parts li { padding: 10px 0; border-top: 0.5px solid var(--border); }
.addon-info__parts li:first-child { border-top: 0; padding-top: 0; }
.addon-info__parts strong { display: block; font-size: 13.5px; color: var(--text); }
.addon-info__parts span { display: block; font-size: 13px; line-height: 1.55; color: var(--text2); margin-top: 3px; }
.addon-info__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 0.5px solid var(--border);
}
.addon-info__foot form { display: contents; }

/* Buy modal — one shared dialog, filled per click from a card's data-buy-*. */
.addon-buy__rows { margin: 0 0 14px; display: grid; gap: 8px; }
.addon-buy__rows > div { display: flex; justify-content: space-between; gap: 12px; font-size: 13.5px; }
.addon-buy__rows dt { color: var(--text3); }
.addon-buy__rows dd { margin: 0; color: var(--text); text-align: right; }
.addon-buy__rows code { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text3); }
.addon-buy__note { font-size: 13px; color: var(--text2); margin: 0; }
.addon-buy__warn { font-size: 13px; color: var(--danger-d); margin: 10px 0 0; }

.addon-toolbar { margin-bottom: 24px; }
.addon-search {
  position: relative;
  display: block;
  max-width: 420px;
}
.addon-search svg {
  position: absolute;
  left: 12px;
  top: 50%;
  width: 16px;
  height: 16px;
  transform: translateY(-50%);
  color: var(--text3);
  pointer-events: none;
}
.addon-search .form-input { padding-left: 36px; }
#addon-search-empty { margin-bottom: 24px; }

/* Show all — the script adds .is-collapsed only once it has a button to undo
   it with, and a search lifts the cut so no match is hidden behind it. */
body:not(.is-addon-searching) .addon-grid.is-collapsed > .addon-card:nth-child(n+9) { display: none; }
.addon-grid__more { margin: -4px 0 24px; }
body.is-addon-searching .addon-grid__more { display: none; }

.addon-licence.is-filtered-out,
.addon-licence .is-filtered-out { display: none; }

/* On a phone a stacked card is ~150px of mostly air — dozens of them per
   licence is a very long scroll. Each tightens instead: icon beside name
   (and tagline), then price and the two buttons along one row beneath. */
@media (max-width: 600px) {
  .addon-grid { gap: 10px; }
  .addon-card {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas: "icon name" "icon tag" "foot foot";
    align-items: center;
    column-gap: 12px;
    row-gap: 2px;
    padding: 14px 16px;
  }
  .addon-card:hover { transform: none; }
  .addon-card__top { grid-area: icon; margin: 0; }
  .addon-card .addon-card__top .badge { display: none; }
  .addon-card .addon-card__name { grid-area: name; font-size: 15px; align-self: end; }
  .addon-card:not(:has(.addon-card__tagline)) .addon-card__name { grid-row: 1 / span 2; align-self: center; }
  .addon-card__tagline { grid-area: tag; font-size: 12.5px; align-self: start; }
  .addon-card__foot {
    grid-area: foot;
    margin-top: 10px;
    padding-top: 10px;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
  .addon-card__foot .addon-card__actions { flex: 0 1 auto; }
  .addon-card__price { font-size: 16px; }
  body:not(.is-addon-searching) .addon-grid.is-collapsed > .addon-card:nth-child(n+6) { display: none; }
}

/* =============================================================
   TABLES
   ============================================================= */

.portal-table {
  width: 100%;
  border-collapse: collapse;
}

.portal-table th {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text3);
  padding: 10px 16px;
  text-align: start;
  border-bottom: 0.5px solid var(--border);
  font-weight: 600;
  font-family: 'DM Sans', sans-serif;
}

.portal-table td {
  padding: 12px 16px;
  color: var(--text2);
  font-size: 13px;
  border-bottom: 0.5px solid var(--border);
}

.portal-table tr:hover td {
  background: var(--bg3);
}

/* =============================================================
   FORMS
   ============================================================= */

.form-group {
  margin-bottom: 16px;
}

.form-label {
  display: block;
  font-size: 12px;
  color: var(--text2);
  margin-bottom: 6px;
  font-weight: 500;
}

.form-input,
.form-select,
.form-textarea {
  width: 100%;
  background: var(--bg3);
  border: 0.5px solid var(--border2);
  border-radius: var(--r);
  padding: 10px 14px;
  color: var(--text);
  font-family: 'DM Sans', sans-serif;
  font-size: 14px;
  transition: border-color 0.15s;
  appearance: none;
  -webkit-appearance: none;
}

.form-input:focus,
.form-select:focus,
.form-textarea:focus {
  border-color: var(--amber);
  outline: none;
}

.form-input::placeholder,
.form-textarea::placeholder {
  color: var(--text3);
}

.form-textarea {
  resize: vertical;
  min-height: 100px;
}

.form-hint {
  font-size: 11px;
  color: var(--text3);
  margin-top: 4px;
}

/* The purchase-key field: its placeholder IS the format spec, so it has to
   stay whole rather than clip — see the mobile override below. */
.key-field {
  font-family: 'DM Mono', monospace;
  font-size: 13px;
  letter-spacing: 0.04em;
}

/* =============================================================
   BUTTONS
   ============================================================= */

.btn-primary,
.btn-secondary,
.btn-danger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 20px;
  border-radius: var(--r);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  font-family: 'DM Sans', sans-serif;
  border: none;
  transition: opacity 0.15s, background 0.15s, color 0.15s;
  text-decoration: none;
  line-height: 1;
  white-space: nowrap;
}

.btn-primary {
  background: var(--amber);
  color: var(--on-amber);
  border: none;
}
.btn-primary:hover {
  opacity: 0.88;
}

.btn-secondary {
  background: transparent;
  color: var(--text2);
  border: 0.5px solid var(--border2);
}
.btn-secondary:hover {
  background: var(--bg3);
  color: var(--text);
}

.btn-danger {
  background: var(--danger-d);
  color: var(--on-danger);
  border: none;
}
.btn-danger:hover {
  opacity: 0.85;
}

.btn-sm {
  padding: 6px 14px;
  font-size: 12px;
}

/* A secondary action with more presence than .btn-secondary: the card's own
   fill inside, an amber-to-teal gradient ring outside. Two backgrounds —
   padding-box paints the fill, border-box the gradient showing through the
   transparent border — so the ring follows the radius with no extra
   element. Same box as the other variants, so it lines up beside them. */
.btn-gradient {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 18px; /* 2px less than the others each way: the ring adds it back */
  border-radius: var(--r);
  font-size: 13px;
  font-weight: 600;
  font-family: 'DM Sans', sans-serif;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  text-decoration: none;
  color: var(--text);
  border: 2px solid transparent;
  background:
    linear-gradient(var(--bg2), var(--bg2)) padding-box,
    linear-gradient(135deg, var(--amber), var(--teal)) border-box;
  transition: background 0.15s, color 0.15s;
}
.btn-gradient:hover {
  background:
    linear-gradient(var(--bg3), var(--bg3)) padding-box,
    linear-gradient(135deg, var(--amber), var(--teal)) border-box;
}
.btn-gradient.btn-sm { padding: 4px 12px; font-size: 12px; }

/* An upgrade button carries a type name and a price, which do not fit one
   line on a phone; it wraps rather than running off the card. */
.product-actions .btn-sm { white-space: normal; text-align: center; overflow-wrap: anywhere; }

/* =============================================================
   BADGES
   ============================================================= */

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 99px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.4;
}

.badge-green {
  background: var(--teal-12);
  color: var(--teal-d);
}

.badge-amber {
  background: var(--amber-bg);
  color: var(--amber-d);
}

.badge-red {
  background: var(--danger-12);
  color: var(--danger-d);
}

.badge-gray {
  background: var(--bg4);
  color: var(--text3);
}

/* =============================================================
   ALERTS
   ============================================================= */

.alert {
  padding: 12px 16px;
  border-radius: var(--r);
  font-size: 13px;
  margin-bottom: 16px;
  line-height: 1.5;
}

.alert-success {
  background: var(--teal-bg);
  border: 0.5px solid var(--teal-20);
  color: var(--teal-d);
}

.alert-error {
  background: var(--danger-08);
  border: 0.5px solid var(--danger-20);
  color: var(--danger-d);
}

.alert-info {
  background: var(--amber-bg);
  border: 0.5px solid var(--amber-20);
  color: var(--amber-d);
}

/* =============================================================
   STATUS ICON — the result circle on payment-success/cancelled
   ============================================================= */

.status-icon {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 24px;
}

.status-icon-warn   { background: var(--amber-14); }
.status-icon-ok     { background: var(--teal-12); }
.status-icon-danger { background: var(--danger-10); }

/* =============================================================
   WALLET CARDS
   ============================================================= */

.wallet-card {
  background: var(--bg2);
  border: 0.5px solid var(--border);
  border-radius: var(--rl);
  padding: 24px;
  border-top: 2px solid var(--border);
}

.wallet-bonus {
  border-top-color: var(--amber);
}

.wallet-earned {
  border-top-color: var(--teal);
}

.wallet-amount {
  font-family: 'Fraunces', serif;
  font-size: 32px;
  color: var(--text);
  line-height: 1.1;
}

.wallet-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text3);
  margin-top: 4px;
}

.wallet-note {
  font-size: 11px;
  color: var(--text3);
  margin-top: 6px;
}

/* ── The single balance, its info button and the breakdown popover ──
   .wallet-balance is the positioning context: the popover is absolute against
   it, not against .wallet-card, so it opens under the figure wherever that
   sits once the header row wraps. .wallet-card has no overflow of its own, so
   nothing clips it -- unlike .portal-card, which needs the
   :has(.dl-menu.is-open) escape hatch further up this file. */
.wallet-balance {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
}

.wallet-balance__figure {
  font-family: var(--display);
  font-size: 36px;
  color: var(--text);
  line-height: 1;
  overflow-wrap: anywhere;
}

.wallet-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--text2);
  cursor: pointer;
}
.wallet-info:hover,
.wallet-info[aria-expanded="true"] {
  background: var(--bg2);
  color: var(--text);
}
.wallet-info:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
}

.wallet-breakdown[hidden] { display: none; }

.wallet-breakdown {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 5;
  min-width: 232px;
  max-width: 80vw;
  padding: 12px 14px;
  border-radius: var(--rl);
  background: var(--bg2);
  border: 0.5px solid var(--border2);
  box-shadow: var(--shadow-card);
}

.wallet-breakdown__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  font-size: 13px;
  color: var(--text3);
  padding: 4px 0;
}
.wallet-breakdown__row strong {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.wallet-breakdown__row--total {
  margin-top: 4px;
  padding-top: 8px;
  border-top: 0.5px solid var(--border2);
  color: var(--text);
}

.wallet-nudge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 10px;
  font-size: 12px;
  color: var(--teal);
  text-decoration: none;
}
.wallet-nudge:hover { text-decoration: underline; }

/* ── Top-up packs ──
   Left-aligned rather than centred: the grant, the price and the button all
   start on the same edge, so a row of packs reads down a column instead of
   asking the eye to re-centre on every card. */
.pack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(212px, 1fr));
  gap: 14px;
}

.pack-card {
  display: flex;
  flex-direction: column;
  padding: 16px;
  border-radius: var(--rl);
  background: var(--bg2);
  border: 0.5px solid var(--border);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.pack-card:hover {
  border-color: var(--border2);
  box-shadow: var(--shadow-card);
}

/* The strongest pack, marked rather than merely bordered. A 2px top rule was
   too quiet to survive a row of four -- the eye reads fill before it reads
   edges, so this tints instead. */
.pack-card--best {
  background: var(--amber-bg);
  border-color: var(--amber);
}

/* Name left, badges right, on one line. No min-height reservation is needed
   any more: the name is always present, so the line always has height and a
   bonus-less pack lines up with the rest for free. */
.pack-card__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  margin-bottom: 8px;
}
.pack-card__flags {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* The pack's own name, as a kicker above the figure. Uppercase and tracked so
   it reads as a label rather than competing with the amount; the unit label
   below the amount is sentence case for the same reason -- two uppercase lines
   bracketing the number made the card shout from both ends. */
.pack-card__name {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text2);
}

.pack-card__grant {
  font-family: var(--display);
  font-size: 28px;
  line-height: 1.05;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* The noun, demoted. As part of the same string at 22px it competed with the
   figure and wrapped "$115.00 Store credit" onto two lines. */
.pack-card__label {
  margin-top: 3px;
  font-size: 12px;
  color: var(--text3);
}

.pack-card__rows {
  margin-top: 10px;
  margin-bottom: 12px;
  padding-top: 9px;
  border-top: 0.5px solid var(--border);
}
.pack-card--best .pack-card__rows {
  border-top-color: rgba(var(--amber-rgb), 0.25);
}

.pack-card__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: var(--text3);
  padding: 2px 0;
}
.pack-card__row strong {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.pack-card__row--extra,
.pack-card__row--extra strong {
  color: var(--teal-d);
}
/* The em dash in a pack with no bonus: present so the row exists and the card
   keeps its neighbours' height, quiet so it does not read as a figure. */
.pack-card__none { color: var(--text3); font-weight: 400; }

.pack-card__cta {
  /* auto, not a fixed gap: a pack with no bonus has one row fewer, so its
     button floated above its neighbours' in a grid that already stretches
     every card to the same height. The minimum gap above it comes from
     .pack-card__rows' margin-bottom -- NOT from padding here, which is the
     button's own inner padding and would push its label off centre. */
  margin-top: auto;
  width: 100%;
  justify-content: center;
}

/* ── Packs on a phone: a swipeable rail, not a stack ──
   Six packs stacked vertically is a scroll no one finishes, and a grid that
   wraps to two columns hides the good tiers below the fold. The rail shows
   two and a half cards: the cut third card IS the affordance -- it says there
   is more sideways without needing an arrow or a dot row to say it.

   The negative margin pulls the rail out to the screen edge and the matching
   padding puts the first card back where the page's text starts, so the
   partial card bleeds off the edge instead of stopping short of it. Both use
   --portal-main-pad, which this breakpoint already sets, so the rail cannot
   drift out of step with the page gutter. */
@media (max-width: 600px) {
  .pack-grid {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    margin-inline: calc(var(--portal-main-pad) * -1);
    padding-inline: var(--portal-main-pad);
    padding-bottom: 4px;
    scroll-padding-inline-start: var(--portal-main-pad);
    scrollbar-width: none;
  }
  .pack-grid::-webkit-scrollbar { display: none; }

  /* 2.5 cards across the visible width W, with two gaps between them:
       2.5w + 2g = W  ->  w = (W - 2g) / 2.5  ->  ~36% at any phone width.
     min-width has to stay BELOW that figure or it silently clamps the card
     wider and the half-card collapses to a sliver -- which is what 148px did
     at 361px, giving 2.2 cards and no visible affordance. */
  .pack-card {
    /* 40% of the rail's content box lands ~2.5 cards in view. Measured, not
       derived: the rail bleeds to the screen edge through negative margins, so
       the width the percentage resolves against is the viewport, not the
       page's text column -- 36% looked right on paper and showed 2.8. */
    flex: 0 0 40%;
    min-width: 120px;
    padding: 14px;
    scroll-snap-align: start;
  }

  /* At ~120px wide there is no room for a label and a figure on one line:
     "Extra credit" and "+$7.50" together need more than the card's content
     box, so the label wrapped mid-phrase. Stacked instead -- label above
     value, label shrunk to a caption -- which holds at any width rather than
     at the one this was measured on. */
  .pack-card__row {
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    font-size: 12px;
  }
  .pack-card__row > span {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
  }

  /* The figure has to fit the narrower card: "$1,500.00" at 28px overflows a
     120px card. */
  /* At ~120px the name and a badge cannot share a line, so the head wraps.
     Aligned left rather than space-between, or the badge is flung to the far
     edge on its own row with a gap the eye reads as a mistake. */
  /* Reserved to two lines: a pack with no badge otherwise has a one-line head
     and, with the buttons bottom-aligned, that shortfall reappears as a gap
     above ITS button. Holding the head equal puts the difference where it
     reads as spacing instead of a void. */
  .pack-card__head {
    justify-content: flex-start;
    align-items: flex-start;
    min-height: 34px;
    margin-bottom: 6px;
  }
  /* Two badges plus a name is three wrapped lines in a 131px card -- 66px of
     head on a 279px card. The best pack is already tinted amber end to end,
     which says the same thing at a glance and costs no height, so the worded
     badge stands down on a phone. */
  .pack-card__flags .badge-amber {
    /* Taken out of the layout, NOT out of the page. display:none would drop
       "Best value" from the accessibility tree, and the thing standing in for
       it -- the amber fill -- is visible only to people who can see it. This
       leaves the words for a screen reader while costing no height. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .pack-card__row { line-height: 1.35; }
  .pack-card__name { font-size: 10px; }
  .pack-card__grant { font-size: 19px; }
  .pack-card__label { font-size: 11px; }
}

/* =============================================================
   EMPTY STATE
   ============================================================= */

.empty-state {
  text-align: center;
  padding: 48px 24px;
}

.empty-icon {
  font-size: 32px;
  margin-bottom: 12px;
}

.empty-title {
  font-family: 'Fraunces', serif;
  font-size: 18px;
  color: var(--text);
  margin: 0;
}

.empty-text {
  font-size: 13px;
  color: var(--text2);
  margin-top: 6px;
  margin-bottom: 0;
}

/* =============================================================
   COPY FIELD
   ============================================================= */

.copy-field {
  display: flex;
  flex-direction: row;
  background: var(--bg3);
  border: 0.5px solid var(--border2);
  border-radius: var(--r);
  overflow: hidden;
}

.copy-field input {
  flex: 1;
  background: transparent;
  border: none;
  padding: 10px 14px;
  color: var(--text);
  font-family: 'DM Mono', monospace;
  font-size: 13px;
  outline: none;
  min-width: 0;
}

.copy-field button {
  background: var(--bg4);
  border: none;
  border-inline-start: 0.5px solid var(--border);
  color: var(--text2);
  padding: 10px 14px;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  font-family: 'DM Sans', sans-serif;
  transition: color 0.15s;
}
.copy-field button:hover {
  color: var(--amber);
}

/* =============================================================
   TOAST
   ============================================================= */

#toast {
  position: fixed;
  bottom: 24px;
  inset-inline-end: 24px;
  background: var(--bg4);
  border: 0.5px solid var(--border2);
  color: var(--text);
  padding: 12px 20px;
  border-radius: var(--rl);
  font-size: 13px;
  font-family: 'DM Sans', sans-serif;
  z-index: 9999;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
  max-width: 320px;
  line-height: 1.4;
}

#toast.toast-visible {
  opacity: 1;
}

#toast.toast-success {
  border-color: var(--teal-30);
  color: var(--teal-d);
}

#toast.toast-error {
  border-color: var(--danger-30);
  color: var(--danger-d);
}

/* =============================================================
   UTILITY CLASSES
   ============================================================= */

.text-amber  { color: var(--amber); }
.text-teal   { color: var(--teal); }
.text-danger { color: var(--danger); }
.text-muted  { color: var(--text2); }
.text-dim    { color: var(--text3); }

.mt-4  { margin-top: 4px; }
.mt-8  { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.mt-32 { margin-top: 32px; }

.flex {
  display: flex;
  align-items: center;
}

.flex-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* A page header's title and its actions: on a phone the actions drop under
   the title instead of squeezing it. */
.page-header.flex-between {
  flex-wrap: wrap;
  gap: 16px;
}

/* More than one action in a page header or an empty state. */
.page-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.page-actions--center {
  justify-content: center;
}
@media (max-width: 768px) {
  .page-header .page-actions {
    width: 100%;
  }
  .page-header .page-actions > * {
    flex: 1 1 0;
    justify-content: center;
  }
}

.flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

.gap-8  { gap: 8px; }
.gap-16 { gap: 16px; }

/* =============================================================
   CUSTOM ORDERS
   ============================================================= */

/* =============================================================
   CONVERSATION LIST — dashboard/custom-orders.php and
   dashboard/support-tickets.php, via includes/conversation_list.php.
   An inbox: one card, one clickable row per conversation, with the
   rows where the customer owes a reply standing out.
   ============================================================= */

.convo-filters {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}
.convo-filters[hidden] { display: none; }
.convo-filter {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px;
  border-radius: 99px;
  border: 1px solid var(--border);
  background: var(--bg2);
  color: var(--text2);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.convo-filter:hover { border-color: var(--text3); color: var(--text); }
.convo-filter.is-active {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg2);
}
.convo-filter__count {
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 99px;
  background: var(--bg4);
  color: var(--text3);
  font-size: 11px;
  text-align: center;
}
.convo-filter.is-active .convo-filter__count { background: var(--bg2); color: var(--text); }

/* The rows supply their own padding, edge to edge, so a hover fill reaches
   the card's border rather than stopping 24px short of it. */
/* Names the kind of conversation in the merged Help inbox — plain text, not a
   badge, so the status badge beside it stays the row's one coloured mark. */
.convo-row__kind {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text3);
}

.convo-card { padding: 0; overflow: hidden; }
.convo-list { list-style: none; margin: 0; padding: 0; }
.convo-item + .convo-item { border-top: 0.5px solid var(--border); }

.convo-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 18px 20px;
  color: inherit;
  text-decoration: none;
  transition: background .15s;
}
.convo-row:hover { background: var(--bg3); }
.convo-row:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }
/* Owes a reply: an amber edge, like an unread thread in a mail client. */
.convo-row.is-your-turn { background: var(--amber-bg); }
.convo-row.is-your-turn::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--amber);
}
.convo-row.is-your-turn:hover { background: var(--amber-bg2); }

.convo-row__avatar {
  flex: 0 0 auto;
  width: 42px;
  height: 42px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.convo-row__avatar svg { width: 20px; height: 20px; }
.convo-row__avatar.is-green { background: var(--teal-12); color: var(--teal-d); }
.convo-row__avatar.is-amber { background: var(--amber-bg2); color: var(--amber-d); }
.convo-row__avatar.is-red   { background: var(--danger-12); color: var(--danger-d); }
.convo-row__avatar.is-gray  { background: var(--bg4); color: var(--text3); }

.convo-row__main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.convo-row__top { display: flex; align-items: baseline; gap: 12px; }
.convo-row__title {
  flex: 1 1 auto;
  min-width: 0;
  font-family: 'Fraunces', serif;
  font-size: 16px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.convo-row.is-your-turn .convo-row__title { font-weight: 600; }
.convo-row__time { flex: 0 0 auto; font-size: 12px; color: var(--text3); white-space: nowrap; }
.convo-row.is-your-turn .convo-row__time { color: var(--amber-d); font-weight: 600; }
.convo-row__preview {
  font-size: 13.5px;
  color: var(--text2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.convo-row.is-closed .convo-row__preview { color: var(--text3); }
.convo-row__you { color: var(--text3); margin-inline-end: 2px; }
.convo-row__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  margin-top: 4px;
  font-size: 12px;
}
.convo-row__turn { color: var(--amber-d); font-weight: 600; }
.convo-row__dim { color: var(--text3); }

.convo-row__chevron {
  flex: 0 0 auto;
  align-self: center;
  width: 18px;
  height: 18px;
  color: var(--text3);
  transition: transform .15s, color .15s;
}
.convo-row:hover .convo-row__chevron { transform: translateX(2px); color: var(--text); }

/* The link's accessible name beyond its visible text — "View conversation". */
.convo-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.convo-empty { margin: 0; padding: 28px 20px; text-align: center; font-size: 13px; color: var(--text3); }

@media (max-width: 600px) {
  .convo-row { padding: 16px; gap: 12px; }
  .convo-row__avatar { width: 36px; height: 36px; border-radius: 10px; }
  .convo-row__avatar svg { width: 18px; height: 18px; }
  .convo-row__chevron { display: none; }
  /* One line is only three or four words at this width — two keep a
     request's title recognisable. */
  .convo-row__title {
    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }
}

/* =============================================================
   CUSTOM ORDER THREAD (chat-style conversation)
   ============================================================= */

/* .portal-card already gives this its surface — bg2, border, radius,
   shadow, 24px padding. This layer only adds the chat structure inside it:
   a message list, then the composer, sharing one card instead of two.

   flex-shrink: 0 for the same reason .co-thread__list below needs it: this
   card is itself a flex child of .portal-main, which is overflow-y: auto —
   so without this, the card's automatic minimum size is 0 and .portal-main
   is free to squeeze it far short of what the list and composer inside it
   actually need, instead of scrolling the page past it as intended. */
.co-thread {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  /* Three edge-to-edge bands — header, tinted conversation, composer — so
     each supplies its own padding instead of the card's 24px. */
  padding: 0;
  overflow: hidden;
}

/* The conversation's header: what this thread is and where it stands. */
.co-thread__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 0.5px solid var(--border);
}
.co-thread__back {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text2);
  transition: background .15s, color .15s;
}
.co-thread__back:hover { background: var(--bg3); color: var(--text); }
.co-thread__back svg { width: 20px; height: 20px; }
.co-thread__head-icon {
  flex: 0 0 auto;
  width: 42px;
  height: 42px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.co-thread__head-icon svg { width: 20px; height: 20px; }
.co-thread__head-icon.is-green { background: var(--teal-12); color: var(--teal-d); }
.co-thread__head-icon.is-amber { background: var(--amber-bg2); color: var(--amber-d); }
.co-thread__head-icon.is-red   { background: var(--danger-12); color: var(--danger-d); }
.co-thread__head-icon.is-gray  { background: var(--bg4); color: var(--text3); }
.co-thread__head-body { min-width: 0; }
.co-thread__title {
  font-family: 'Fraunces', serif;
  font-size: 18px;
  font-weight: 500;
  color: var(--text);
  margin: 0 0 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.co-thread__meta { display: flex; align-items: center; gap: 8px; margin: 0; }
.co-thread__ref { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text3); }

/* Its own scroll region, like any chat app — the composer below it (a sibling
   in .co-thread, not a child here) has to stay on screen no matter how long
   the conversation gets, and the page itself must never grow past the
   header, the order title and the composer all being visible at once. */
.co-thread__list {
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  height: clamp(360px, calc(100vh - 360px), 640px);
  /* .co-thread is itself a flex column, and a flex item with overflow other
     than visible gets an automatic minimum size of 0 — without this, .co-
     thread__list is free to shrink well under the height above (down to
     whatever the rest of the page happens to leave it) the moment anything
     above or below it on the page is tall enough to make that page-level
     flex column feel short on room, which silently defeats the fixed-height
     scroll region this rule exists to create. */
  flex-shrink: 0;
  overscroll-behavior: contain;
  padding: 20px 24px 24px;
  background: var(--bg);
}

@media (max-width: 480px) {
  /* A short letterbox is worse on a phone, where the viewport is the whole
     budget — give the list the height the chrome around it actually leaves
     rather than the desktop clamp's floor. 460px covers the mobile topbar
     and quick-nav row above the card, the composer below the list, and
     .portal-bottomnav — a fixed 62px bar that overlaps the foot of the
     viewport on top of everything else, not just more chrome to subtract
     inline. */
  .co-thread__list {
    height: calc(100vh - 460px);
  }
}

.co-thread__empty {
  padding: 32px 0;
}

/* Centred, muted divider between days. */
.co-thread__day {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 22px 0 14px;
}
.co-thread__day::before,
.co-thread__day::after {
  content: "";
  flex: 1;
  height: 0.5px;
  background: var(--border2);
}
.co-thread__list > .co-thread__day:first-child {
  margin-top: 0;
}

.co-thread__day-label {
  font-family: 'DM Sans', sans-serif;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text3);
  padding: 0;
}

/* author_type = 'system', and any message whose kind isn't plain 'message'
   (a quote/invoice/delivery card, later), is not somebody talking — centred
   and muted across the row's width, never a bubble, so a later phase can
   post structured cards into this same thread without them reading as
   dialogue. */
.co-thread__event {
  align-self: center;
  max-width: 85%;
  margin: 8px 0;
  padding: 5px 12px 5px 10px;
  border-radius: 999px;
  border: 0.5px solid var(--border);
  background: var(--bg2);
  color: var(--text3);
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
  overflow-wrap: anywhere;
}
.co-thread__event::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-inline-end: 8px;
  border-radius: 50%;
  background: var(--text3);
  vertical-align: middle;
}

/* A quote card: the same centred lane as .co-thread__event above (it is not
   somebody talking either), but wide and structured enough to carry line
   items, a total and — while still answerable — its own controls. Built
   entirely from tokens and classes this file already defines. */
.co-thread__quote {
  align-self: center;
  width: 100%;
  max-width: 420px;
  margin: 12px 0;
  padding: 16px 18px;
  border-radius: var(--rl);
  background: var(--bg2);
  border: 0.5px solid var(--border);
  border-inline-start: 3px solid var(--border2);
  box-shadow: var(--shadow-card);
  text-align: start;
}
/* The accent follows the card's own status badge, so it can never disagree
   with it: waiting on the customer, settled, or called off. */
.co-thread__quote:has(.badge-amber) { border-inline-start-color: var(--amber); }
.co-thread__quote:has(.badge-green) { border-inline-start-color: var(--teal); }
.co-thread__quote:has(.badge-red)   { border-inline-start-color: var(--danger); }

/* A settled quote (accepted, declined or superseded) cannot be acted on
   again — dimmed so it reads as history, not as something still asking for
   a decision. */
.co-thread__quote--muted {
  opacity: 0.72;
}

.co-thread__quote-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 12px;
}

.co-thread__quote-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}

.co-thread__quote-lines {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
}

.co-thread__quote-line {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  border-bottom: 0.5px solid var(--border2);
  font-size: 13px;
}
.co-thread__quote-line:last-child {
  border-bottom: none;
}

.co-thread__quote-line-desc {
  color: var(--text2);
  overflow-wrap: anywhere;
}

.co-thread__quote-line-qty {
  color: var(--text3);
  font-size: 11px;
  margin-inline-start: 4px;
}

.co-thread__quote-line-amount {
  flex-shrink: 0;
  color: var(--text);
  font-weight: 600;
  white-space: nowrap;
}

.co-thread__quote-total {
  display: flex;
  justify-content: space-between;
  padding-top: 10px;
  margin-top: 4px;
  border-top: 0.5px solid var(--border2);
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
}

.co-thread__quote-terms {
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 0.5px solid var(--border2);
  font-size: 12px;
  line-height: 1.5;
  color: var(--text2);
}

.co-thread__quote-emailed,
.co-thread__quote-note {
  margin: 10px 0 0;
  font-size: 11px;
  color: var(--text3);
}

.co-thread__quote-form {
  margin-top: 14px;
}

.co-thread__quote-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

/* An invoice card: same centred lane and card shell as .co-thread__quote
   above (it announces a milestone the same way a quote card announces an
   offer), just without the muted/settled state that quote gets — an invoice
   still shows its own status badge either way. */
.co-thread__milestone {
  align-self: center;
  width: 100%;
  max-width: 420px;
  margin: 12px 0;
  padding: 16px 18px;
  border-radius: var(--rl);
  background: var(--bg2);
  border: 0.5px solid var(--border);
  border-inline-start: 3px solid var(--border2);
  box-shadow: var(--shadow-card);
  text-align: start;
}
/* The accent follows the card's own status badge, so it can never disagree
   with it: waiting on the customer, settled, or called off. */
.co-thread__milestone:has(.badge-amber) { border-inline-start-color: var(--amber); }
.co-thread__milestone:has(.badge-green) { border-inline-start-color: var(--teal); }
.co-thread__milestone:has(.badge-red)   { border-inline-start-color: var(--danger); }

.co-thread__milestone-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}

.co-thread__milestone-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}

.co-thread__milestone-amount {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}

.co-thread__milestone-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

.co-thread__milestone-note {
  margin: 10px 0 0;
  font-size: 11px;
  color: var(--text3);
}

.co-thread__milestone-error {
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--danger-d);
}

/* One row per message. Tight by default (a run of consecutive messages from
   the same author reads as one turn); co-thread__row--start — the first row
   of a run — gets the larger gap that marks a new turn or a new speaker, and
   is the only one that prints the author label above its bubble. */
.co-thread__row {
  display: flex;
  min-width: 0;
  flex-direction: column;
  max-width: min(75%, 480px);
  margin-top: 3px;
}
.co-thread__row--start {
  margin-top: 10px;
}

.co-thread__row--mine {
  align-self: flex-end;
  align-items: flex-end;
}
.co-thread__row--theirs {
  align-self: flex-start;
  align-items: flex-start;
  position: relative;
  /* Every row of theirs keeps the avatar's column, so a run's later bubbles
     line up under its first even though only the first shows the avatar. */
  padding-inline-start: 40px;
}
.co-thread__avatar {
  position: absolute;
  inset-inline-start: 0;
  /* Level with the bubble, below the author label above it. */
  top: 20px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--amber-bg2);
  color: var(--amber-d);
  font-size: 12px;
  font-weight: 700;
}

.co-thread__author {
  font-size: 11px;
  font-weight: 600;
  color: var(--text3);
  margin-bottom: 4px;
  padding: 0 4px;
}

/* overflow-wrap: anywhere (not just break-word) so one unbroken token — a
   pasted URL — wraps inside the bubble instead of forcing max-width to lose
   and blowing out the card. */
.co-thread__bubble {
  max-width: 100%;
  border-radius: 16px;
  padding: 8px 12px 7px 14px;
  /* The time rides at the end of the text's last line when there is room
     (and drops below it when there is not), so a one-word reply is one line
     tall instead of two. An image takes the full row above both. */
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  column-gap: 10px;
  box-shadow: var(--shadow-card);
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* The customer's own bubble: the amber accent, --on-amber for legible text
   on it. Flattened toward the sender's corner — the same "this one points at
   whoever sent it" cue every chat app uses — with a logical property so it
   still points the right way under RTL. */
.co-thread__bubble--mine {
  background: var(--amber);
  color: var(--on-amber);
  border-end-end-radius: 4px;
}
.co-thread__bubble--theirs {
  background: var(--bg2);
  border: 0.5px solid var(--border);
  color: var(--text);
  border-end-start-radius: 4px;
}

.co-thread__body {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
}

/* One attached image inside a bubble, shown above the caption (co-thread__body
   renders only when body is non-empty, so an image-only message is just
   this). Rounded a touch less than the bubble itself — var(--r), not --rl —
   so it reads as content sitting inside the bubble, not another bubble. */
.co-thread__attachment {
  display: block;
  margin-bottom: 6px;
  /* A <button> now, not an <a> — opens the lightbox in place instead of
     navigating away. Reset to plain, unstyled chrome so it reads as the
     image sitting in the bubble, same as it did as a link. */
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.co-thread__attachment img {
  display: block;
  /* width:100%; height:auto does NOT coordinate with max-height the way this
     needs: the browser resolves width to the 280px cap first, then computes
     height from THAT width alone — for a narrow, very tall screenshot, that
     height can be thousands of pixels, well past max-height, and nothing
     pulls the width back down to compensate. width/height:auto together with
     object-fit:contain is what actually fits an image inside both caps at
     once, preserving its aspect ratio either way. */
  width: auto;
  height: auto;
  max-width: 280px;
  max-height: 280px;
  object-fit: contain;
  border-radius: var(--r);
}

/* Full-size view of whatever attachment was clicked. One instance per page,
   reused by every message — see the click handler beside it in thread.php,
   which just swaps the <img> src rather than opening a new element per
   image. [hidden] beats display:flex below on specificity only because it's
   listed first; the same ordering the emoji popover needs for the same
   reason. */
.co-thread__lightbox[hidden] { display: none; }
.co-thread__lightbox {
  position: fixed;
  inset: 0;
  /* Above every layer of the dashboard chrome, not just the thread card:
     .pt-top sits at 120 and an open .pt-menu/.pt-nav__panel at 130
     (chrome.css) — both would otherwise paint over this backdrop instead of
     being covered by it, since z-index only wins within a stacking context
     if it is actually the higher number. */
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(0, 0, 0, 0.82);
  cursor: zoom-out;
}
.co-thread__lightbox-img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--r);
  cursor: default;
}
.co-thread__lightbox-close {
  position: absolute;
  top: 16px;
  right: 20px;
  width: 40px;
  height: 40px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.co-thread__lightbox-close:hover {
  background: rgba(255, 255, 255, 0.22);
}

.co-thread__time {
  display: block;
  margin-inline-start: auto;
  padding-bottom: 1px;
  font-size: 10.5px;
  line-height: 1.5;
  text-align: end;
  white-space: nowrap;
}
.co-thread__bubble > .co-thread__attachment { flex-basis: 100%; }
/* An image bubble is a thin frame round the picture, not a thick one: the
   bubble's own padding drops away and the text and time under the image
   take a little back for themselves. */
/* Block, not the text bubble's flex row: a wrapping flex box sizes itself
   to image + text + time laid end to end before it wraps them, leaving the
   bubble wider than the picture it frames. */
.co-thread__bubble:has(> .co-thread__attachment) { display: block; padding: 4px; }
.co-thread__bubble:has(> .co-thread__attachment) > .co-thread__attachment { margin-bottom: 0; }
.co-thread__bubble:has(> .co-thread__attachment) > .co-thread__attachment img { border-radius: 12px; }
.co-thread__bubble:has(> .co-thread__attachment) > .co-thread__body { padding: 5px 8px 2px; }
.co-thread__bubble:has(> .co-thread__attachment) > .co-thread__time { padding: 4px 8px 3px; }
.co-thread__bubble > .co-thread__body { flex: 0 1 auto; min-width: 0; }
.co-thread__bubble--mine .co-thread__time { color: var(--on-amber); opacity: 0.8; }
.co-thread__bubble--theirs .co-thread__time { color: var(--text3); }

/* The composer: bottom of the same card as the conversation, not a second
   detached one below it. */
.co-thread__composer {
  border-top: 0.5px solid var(--border);
  padding: 14px 20px 16px;
  background: var(--bg2);
}
.co-thread__empty + .co-thread__composer {
  margin-top: 0;
}

.co-thread__form-row {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  padding: 6px;
  border: 1px solid var(--border2);
  border-radius: 14px;
  background: var(--bg);
  transition: border-color .15s, box-shadow .15s;
}
.co-thread__form-row:focus-within {
  border-color: var(--amber);
  box-shadow: 0 0 0 3px var(--amber-bg);
}

/* Beats .form-textarea's own box: inside the bar the input is borderless,
   the bar itself is the field. */
.co-thread__form-row .co-thread__input {
  border: 0;
  background: transparent;
  box-shadow: none;
  outline: none;
  resize: none;
  padding: 8px 6px;
}
.co-thread__input {
  flex: 1;
  /* A flex item's automatic minimum is its content's intrinsic size, and a
     textarea's is its default column width — without this the composer
     refuses to shrink, pushing the whole card wider than a phone screen and
     taking the right-aligned bubbles off the edge with it. */
  min-width: 0;
  min-height: 36px;
  max-height: 160px;
}

.co-thread__send {
  flex-shrink: 0;
  border-radius: 10px;
  padding: 10px 16px;
}
.co-thread__send svg { width: 16px; height: 16px; }

.co-thread__hint {
  margin: 6px 2px 0;
  font-size: 12px;
  color: var(--text3);
}

/* Attach/emoji: plain icon buttons sized to match the textarea's own
   min-height, never a bespoke button style of their own. */
.co-thread__icon-btn {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--text3);
  cursor: pointer;
  transition: background .15s, color .15s;
}
.co-thread__icon-btn:hover { background: var(--bg3); color: var(--text); }
.co-thread__icon-btn svg { width: 19px; height: 19px; }

@media (max-width: 480px) {
  .co-thread__head { padding: 12px 14px; gap: 10px; }
  .co-thread__head-icon { display: none; }
  .co-thread__list { padding: 14px 12px 18px; }
  .co-thread__row--theirs { padding-inline-start: 34px; }
  .co-thread__avatar { width: 26px; height: 26px; font-size: 11px; }
  .co-thread__composer { padding: 10px 12px 12px; }
  /* Icon-only Send on a phone: the bar needs the width for the message. */
  .co-thread__send span { display: none; }
  .co-thread__send { padding: 10px 12px; }
}

.co-thread__emoji-wrap {
  position: relative;
  flex-shrink: 0;
}

/* Before the display rule below, and matched on the attribute so it wins on
   specificity: [hidden] is only display:none in the browser's own sheet, so
   any author display beats it and the popover renders permanently open. */
.co-thread__emoji-popover[hidden] { display: none; }

.co-thread__emoji-popover {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  z-index: 5;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  width: 252px;
  max-width: 80vw;
  padding: 8px;
  border-radius: var(--rl);
  background: var(--bg2);
  border: 0.5px solid var(--border2);
  box-shadow: var(--shadow-card);
}
.co-thread__emoji-popover button {
  border: 0;
  background: none;
  font-size: 18px;
  line-height: 1;
  padding: 6px;
  border-radius: var(--r);
  cursor: pointer;
}
.co-thread__emoji-popover button:hover {
  background: var(--bg3);
}

/* The chosen-file preview, shown above the input row once a file is picked
   — see the composer script. Not a bubble: this is still being composed,
   never rendered as a sent message. Before the display rule below, and
   matched on the attribute so it wins on specificity: [hidden] is only
   display:none in the browser's own sheet, so any author display beats it
   and the preview renders permanently open. */
.co-thread__attach-preview[hidden] { display: none; }

.co-thread__attach-preview {
  position: relative;
  display: inline-block;
  margin-bottom: 8px;
}
.co-thread__attach-preview-img {
  display: block;
  max-width: 120px;
  max-height: 120px;
  border-radius: var(--r);
  border: 0.5px solid var(--border2);
}
.co-thread__attach-remove {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 0.5px solid var(--border2);
  background: var(--bg2);
  color: var(--text);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}

/* Visually hidden but still read by a screen reader — the composer shows its
   label as a placeholder instead, the way a chat app's message box does. */
.co-thread__sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* =============================================================
   CUSTOM ORDER QUOTE DOCUMENT — the page a customer keeps
   ============================================================= */

/* The whole page's content, not a bubble in a scrolling list the way
   .co-thread__quote (above) is — wider, and centred like a document rather
   than pinned to one side of a conversation. Also the print marker
   @media print targets below, via body:has(.qdoc). */
.qdoc {
  max-width: 640px;
  margin: 0 auto;
}

.qdoc__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 16px;
  padding-bottom: 20px;
  margin-bottom: 20px;
  border-bottom: 0.5px solid var(--border2);
}

.qdoc__meta-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.qdoc__meta-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text3);
}

.qdoc__meta-value {
  font-size: 14px;
  color: var(--text);
  font-weight: 600;
}

.qdoc__lines {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
}

.qdoc__line {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 0;
  border-bottom: 0.5px solid var(--border2);
  font-size: 14px;
}
.qdoc__line:last-child {
  border-bottom: none;
}

.qdoc__line-desc {
  color: var(--text2);
  overflow-wrap: anywhere;
}

.qdoc__line-qty {
  color: var(--text3);
  font-size: 12px;
  margin-inline-start: 4px;
}

.qdoc__line-amount {
  flex-shrink: 0;
  color: var(--text);
  font-weight: 600;
  white-space: nowrap;
}

.qdoc__total {
  display: flex;
  justify-content: space-between;
  padding-top: 14px;
  margin-top: 4px;
  border-top: 1px solid var(--border);
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
}

.qdoc__terms {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 0.5px solid var(--border2);
  font-size: 13px;
  line-height: 1.6;
  color: var(--text2);
}

.qdoc__accepted {
  margin-top: 20px;
}

/* =============================================================
   PRINT — the quote document, and nothing else on the page
   ============================================================= */

/* Scoped the same way the mobile conversation layout above scopes itself —
   body:has(.qdoc) rather than a flag threaded through the shared chrome, so
   portal_header.php/portal_footer.php need not know this page exists. A
   browser without :has() prints the chrome alongside the document instead
   of failing to print at all.

   Every element portal_header.php, portal_chrome() and portal_footer.php
   render outside the page's own content, and the selector below that hides
   it — read from those three files directly, not from memory, after an
   earlier pass missed one:
     <header class="pt-top">          → .pt-top
     <nav class="pt-nav">             → .pt-nav (portal_chrome()'s own second
                                         row, a SIBLING of .pt-top, not a
                                         child of it — this is the one an
                                         earlier version of this rule missed:
                                         hiding .pt-top alone never touches
                                         a sibling)
     <nav class="portal-sidebar">     → .portal-sidebar
     <div class="sidebar-scrim">      → .sidebar-scrim
     <nav class="portal-bottomnav">   → .portal-bottomnav (nested inside
                                         .portal-main in the DOM, but still
                                         chrome, not document)
     <footer class="pt-foot">         → .pt-foot (also nested inside
                                         .portal-main)
   The one further element portal_header.php can render, the guest sign-in/
   register modal (includes/auth_modal.php, only for a signed-out visitor),
   needs no rule here: it carries the native `hidden` attribute and no
   stylesheet anywhere in this codebase overrides it, so it is already
   invisible unless a script removes that attribute — which printing does
   not do. */
@media print {
  body:has(.qdoc) .pt-top,
  body:has(.qdoc) .pt-nav,
  body:has(.qdoc) .portal-sidebar,
  body:has(.qdoc) .sidebar-scrim,
  body:has(.qdoc) .portal-bottomnav,
  body:has(.qdoc) .pt-foot,
  body:has(.qdoc) .pt-foot-gap {
    display: none;
  }

  /* .portal-body and .portal-main size themselves against the viewport and
     clip to it with overflow-y: auto for the on-screen scrolling layout —
     exactly what a printed page must not do, or only whatever fit the last
     on-screen scroll position would ever reach paper. */
  body:has(.qdoc) .portal-body {
    display: block;
    height: auto;
  }
  body:has(.qdoc) .portal-main {
    display: block;
    overflow: visible;
    /* The variable too, not just the shorthand — .pt-foot cancels the variable,
       so leaving it at 32px here would hang the footer off both edges. */
    --portal-main-pad: 0px;
    padding: var(--portal-main-pad);
  }

  body:has(.qdoc) .portal-card {
    background: none;
    border: none;
    padding: 0;
  }
}

/* =============================================================
   RESPONSIVE — Mobile (≤ 768px)
   ============================================================= */

@media (max-width: 768px) {
  /* Show mobile menu button */
  .mobile-menu-btn {
    display: flex;
  }

  /* Declutter the top bar — name/logout move into the drawer footer,
     the avatar alone is enough of an identity cue up here. A guest has
     no drawer, so their sign-in link is .topbar-signin, not
     .topbar-logout, and stays visible here. */
  .topbar-username,
  .topbar-logout {
    display: none;
  }

  /* Stack body vertically */
  .portal-body {
    flex-direction: column;
    height: auto;
    min-height: calc(100vh - 116px); /* .pt-top (60) + the quick-jump pills row (56) at this width — .pt-nav is .pt-nav--bare here and chrome.css drops it */
  }

  /* Sidebar becomes a floating rounded drawer, matching the off-canvas
     panel style used by wowcodes.in's own mobile nav (nav.php). */
  .portal-sidebar {
    width: min(300px, 84vw);
    position: fixed;
    top: 12px;
    inset-inline-start: 12px;
    bottom: 12px;
    z-index: 95;
    background: var(--bg2);
    border: 0.5px solid var(--border2);
    border-radius: 20px;
    box-shadow: 0 20px 60px var(--shadow-50);
    /* Parked far enough off-screen that its 60px shadow goes with it —
       24px left a grey band down the page's leading edge while closed. */
    transform: translateX(calc(-100% - 96px));
    transition: transform 0.28s cubic-bezier(.4,0,.2,1);
    height: auto;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 0;
  }

  .portal-sidebar.sidebar-open {
    transform: translateX(0);
  }

  .sidebar-mobile-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 16px 12px;
    border-bottom: 0.5px solid var(--border);
    flex-shrink: 0;
  }

  .sidebar-nav {
    flex: 1;
    overflow-y: auto;
    padding: 12px 12px;
  }

  .sidebar-mobile-footer {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 16px calc(14px + env(safe-area-inset-bottom));
    border-top: 0.5px solid var(--border);
    flex-shrink: 0;
  }

  /* Dimmed, blurred backdrop behind the open drawer — the drawer now
     floats above the whole viewport, not just below the top bar. */
  .sidebar-scrim {
    display: block;
    position: fixed;
    inset: 0;
    background: var(--scrim);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    z-index: 90;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
  }
  .portal-sidebar.sidebar-open + .sidebar-scrim {
    opacity: 1;
    pointer-events: all;
  }

  /* Fixed tab bar along the bottom */
  .portal-bottomnav {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 100;
    background: var(--bg-96);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-top: 0.5px solid var(--amber-25);
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
  }

  /* A conversation is a screen of its own on a phone. The fixed tab bar and
     the marketing foot both eat room the composer needs, and a chat with a
     licence blurb sitting under it reads as a widget parked on a page rather
     than somewhere to talk. The top bar stays, so getting out is one tap.

     Scoped with :has() rather than a flag in the shared chrome, which is
     being reworked elsewhere; a browser without :has() keeps today's layout. */
  body:has(.co-thread) .portal-bottomnav,
  body:has(.co-thread) .pt-foot,
  body:has(.co-thread) .pt-foot-gap {
    display: none;
  }

  /* The tab bar's 78px reservation is no longer needed here. */
  body:has(.co-thread) .portal-main {
    padding-bottom: 16px;
  }

  /* ...and the list takes back the space that frees. */
  body:has(.co-thread) .co-thread__list {
    height: calc(100vh - 330px);
  }

  /* Main area takes full width, leaves room for the bottom nav */
  .portal-main {
    --portal-main-pad: 16px;
    padding: var(--portal-main-pad) var(--portal-main-pad) calc(78px + env(safe-area-inset-bottom));
    min-height: calc(100vh - 116px); /* same 116 as .portal-body above */
  }

  /* Collapse grids to single column */
  .portal-grid-2,
  .portal-grid-3,
  .portal-grid-4,
  .split-panel {
    grid-template-columns: 1fr;
  }
}

/* =============================================================
   CHECKOUT / CREDITS-CHECKOUT (shared)
   ============================================================= */

/* .co-panel, .co-summary, .co-summary-price and .co-pay moved to
   portal/checkout.css — checkout.php was their only renderer. What's left
   here (.co-error, .gw-tab*) is rendered by BOTH checkout.php AND
   credits-checkout.php (see credits-checkout.php's own gateway-tabs block),
   which is exactly why it stays in this shared dashboard sheet rather than
   moving with the rest: a class that looks checkout-only by name isn't
   always checkout-only by use — verified by grepping every .php file for
   each selector before moving anything, the way .shop-cart-add turned out
   to belong to the product page despite its name. */

.co-error {
  background: var(--danger-08);
  border: 1px solid var(--danger);
  color: var(--danger-d);
  border-radius: var(--r);
  padding: 10px 12px;
  font-size: 13px;
}

/* Gateway-selection tabs (checkout.php, credits-checkout.php) */
.gw-tab {
    transition: border 0.15s, background 0.15s, color 0.15s;
    flex: 1;
    min-width: 100px;
    border: 1px solid var(--border);
    border-radius: var(--r);
    padding: 14px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    background: transparent;
    color: var(--text2);
}
.gw-tab.active {
    border-color: var(--base);
    background: var(--base-bg);
    color: var(--base-d);
}
.gw-tab-sub {
    font-size: 11px;
    font-weight: 400;
    color: inherit;
    opacity: 0.75;
    margin-top: 2px;
}
/* =============================================================
   TOP-UP MODAL
   ============================================================= */

/* Before the display rule below, and matched on the attribute so it wins on
   specificity: [hidden] is only display:none in the browser's own sheet, so any
   author display beats it and the dialog renders permanently open. */
.topup-modal[hidden] { display: none; }

.topup-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.topup-modal__scrim {
  position: absolute;
  inset: 0;
  background: var(--scrim);
}
.topup-modal__panel {
  position: relative;
  width: 100%;
  max-width: 26rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--rl);
  box-shadow: var(--shadow);
  padding: 24px;
}
.topup-modal__title {
  font-family: var(--display);
  font-size: 21px;
  font-weight: 600;
  margin: 0 0 16px;
  color: var(--text);
}

/* Wraps rather than scrolls: four presets at 375px need two rows, and a row
   that scrolls sideways hides the amounts it exists to offer. */
.topup-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 16px;
}
.topup-preset {
  flex: 1 1 5rem;
  padding: 9px 12px;
  border-radius: var(--r);
  border: 1px solid var(--border);
  background: var(--bg2);
  color: var(--text2);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
}
.topup-preset:hover { border-color: var(--base); color: var(--base-d); }
.topup-preset.is-on {
  border-color: var(--base);
  background: var(--base-bg);
  color: var(--base-d);
}

.topup-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text3);
  margin-bottom: 6px;
}
.topup-input {
  width: 100%;
  padding: 11px 12px;
  font-size: 15px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.topup-input:focus-visible { outline: 2px solid var(--base); outline-offset: 1px; }
.topup-hint { margin: 8px 0 0; font-size: 12px; color: var(--text3); }

.topup-actions {
  display: flex;
  gap: 10px;
  margin-block-start: 20px;
}
.topup-actions .btn-primary,
.topup-actions .btn-secondary { flex: 1; justify-content: center; }

/* A native <dialog> so Escape, focus trapping and the backdrop come from the
   browser rather than from script this portal would otherwise have to carry —
   it has no modal library and only one confirm() helper. Everything inside
   reuses the card, badge and button classes above; this rule only gives the
   element a width, a surface and a backdrop it does not have by default. */
.portal-dialog {
    max-width: 520px;
    width: calc(100% - 32px);
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--bg2);
    color: var(--text);
    padding: 24px;
}
.portal-dialog::backdrop {
    /* The same scrim the account drawer and the shop's sheet use. The literal
       this replaced would not have followed a theme flip, and --line / --card
       (what this rule was written against on its own branch) are declared
       nowhere in this portal at all, so the border and surface were resolving
       to nothing. No colour is named in this comment on purpose: the sheet
       scan reads comments too, and a keyword here counts as a literal. */
    background: var(--scrim);
}

/* =============================================================
   SUPPORT TICKET PRIORITY
   ============================================================= */

/* The header's meta line gains a badge and an info button on a support
   ticket; let it wrap on a narrow screen rather than squeeze them. */
.co-thread__meta:has(.st-target, .st-priority-badge) { flex-wrap: wrap; row-gap: 6px; }
.co-thread__head:has(.st-target) { position: relative; }

.st-priority-badge {
  gap: 4px;
  background: var(--amber-bg2);
  color: var(--amber-d);
}
.st-priority-badge svg { width: 11px; height: 11px; flex: 0 0 auto; }

/* The (i) button and its popover. The popover hangs off the whole header,
   not the button, so it can never run off the side of a phone screen. */
.st-target { display: inline-flex; }
.st-target__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text3);
  cursor: pointer;
  transition: background .15s, color .15s;
}
.st-target__btn:hover,
.st-target.is-open .st-target__btn { background: var(--bg3); color: var(--text); }
.st-target__btn svg { width: 16px; height: 16px; }
.st-target__pop {
  display: none;
  position: absolute;
  top: calc(100% - 6px);
  inset-inline-start: 16px;
  z-index: 20;
  width: min(360px, calc(100% - 32px));
  padding: 10px 12px;
  border-radius: var(--rl);
  border: 0.5px solid var(--border);
  background: var(--bg2);
  box-shadow: var(--shadow-card-hover);
  color: var(--text2);
  font-size: 12.5px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.st-target.is-open .st-target__pop,
.st-target:not(.is-js):focus-within .st-target__pop { display: block; }

/* Where the ticket became priority: centred in the conversation like an
   event line, but a full sentence addressed to the customer, so it wraps
   instead of pill-shaping itself. */
.st-priority-banner {
  align-self: center;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  max-width: 460px;
  margin: 10px 0 14px;
  padding: 10px 14px;
  border-radius: var(--rl);
  border: 0.5px solid var(--border);
  border-inline-start: 3px solid var(--amber);
  background: var(--amber-bg);
  color: var(--text);
}
.st-priority-banner svg { width: 16px; height: 16px; flex: 0 0 auto; margin-top: 2px; color: var(--amber-d); }
.st-priority-banner p { margin: 0; font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }

/* On each of the customer's own messages on a priority ticket, beside the
   time — in the bubble's own text colour so it reads on the accent. */
.st-priority-tag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  align-self: flex-end;
  padding-bottom: 1px;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 1.5;
  white-space: nowrap;
  opacity: 0.9;
}
.st-priority-tag svg { width: 10px; height: 10px; }
.co-thread__bubble--mine .st-priority-tag { color: var(--on-amber); margin-inline-start: auto; }
.co-thread__bubble--mine .st-priority-tag + .co-thread__time { margin-inline-start: 6px; }
.co-thread__bubble:has(> .co-thread__attachment) > .st-priority-tag { padding: 4px 8px 0; }

/* The inbox row's badge, beside the status. */
.convo-row__priority { gap: 4px; background: var(--amber-bg2); color: var(--amber-d); }
.convo-row__priority svg { width: 10px; height: 10px; }

/* A support ticket's title wraps rather than cutting off: it is the one
   line saying what the conversation is about. Scoped to the support-ticket
   header (the one carrying .st-target) so the custom-order thread is left
   exactly as it was. */
.co-thread__head:has(.st-target, .st-priority-badge) .co-thread__title {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
}

/* Dashboard Add-ons opened for one add-on (?buy=, from a theme page on the
   shop): just that add-on and the licences it can go on. */
.addon-focus { margin-bottom: 28px; display: flex; flex-direction: column; gap: 14px; }
.addon-focus__head { display: flex; align-items: center; gap: 14px; }
.addon-focus__title h2 { margin: 0; font-size: 1.25rem; font-weight: 700; }
.addon-focus__title p { margin: 2px 0 0; font-weight: 600; color: var(--text2); }
.addon-focus__note { margin: 0; color: var(--text2); line-height: 1.55; }
.addon-focus__licences { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.addon-focus__licence {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px;
  padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px;
}
.addon-focus__licence > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.addon-focus__licence code { font-size: 12px; color: var(--text2); overflow-wrap: anywhere; }
.addon-focus__actions { display: flex; flex-wrap: wrap; gap: 10px; }
.addon-focus__all { align-self: flex-start; font-size: 14px; font-weight: 600; }
