/* Funess design tokens and the handful of components built on them.
 *
 * Before this file the project's entire authored CSS was 236 bytes
 * (badge_notification.css, which contained a `//` line -- a CSS parse error --
 * and set a border-radius that every single usage then overrode inline). Every
 * other visual decision lived in a style="" attribute; there were several
 * hundred, three different purples, and seven different corner radii.
 *
 * Materialize 1.0.0 still provides the grid and the JS widgets. This layer sits
 * on top of it and is the only place a colour or a radius should be named.
 */

:root {
    /* --- Colour ---------------------------------------------------------
     * Three purples were in play: Materialize deep-purple #673AB7 (190 uses),
     * #7705B9 (the PWA manifest's theme_color) and #fff (the theme-color meta).
     * One brand purple now, plus a darker ink variant that actually passes
     * 4.5:1 on white -- #673AB7 as *text* is 5.0:1, but only just, and it was
     * being used at small sizes over grey panels.
     */
    --brand:            #673AB7;
    --brand-dark:       #512DA8;
    --brand-ink:        #5B21B6;   /* brand purple as body-sized text */
    --brand-on:         #ffffff;

    /* #ff5722 meant danger (Close my account), primary (the Settings logout
     * button), accent (savings figures) and notification badge, all at once.
     * Those are four different jobs and they now have four different names. */
    --danger:           #C62828;
    --danger-ink:       #B71C1C;
    --warning:          #E65100;
    --positive:         #2E7D32;
    --accent:           #FF5722;   /* attention only: badges, open-seat counts */

    --ink:              #212121;
    --ink-muted:        #424242;
    --ink-subtle:       #616161;   /* the lightest text allowed anywhere */
    --surface:          #ffffff;
    --surface-sunken:   #F5F5F5;
    --line:             #DADADA;   /* borders were #EEE: ~1.05:1, invisible */

    /* --- Shape --- */
    --radius-card:      20px;
    --radius-pill:      30px;
    --radius-input:     12px;

    /* --- Type --- */
    --font-display: "Montserrat Alternates", "Segoe UI", system-ui, sans-serif;
    --font-body: "Hind Vadodara", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --step--1: .82rem;
    --step-0:  1rem;
    --step-1:  1.25rem;
    --step-2:  1.6rem;
    --step-3:  2.1rem;

    /* --- Spacing --- */
    --space-1: .25rem;
    --space-2: .5rem;
    --space-3: 1rem;
    --space-4: 1.5rem;
    --space-5: 2.5rem;

    --measure: 34rem;      /* readable line length for prose */
    --page-max: 72rem;     /* desktop content width */
}

/* --- Brand chrome ------------------------------------------------------- */
.brand-bar { background-color: var(--brand); }
.brand-wordmark { font-family: var(--font-display); font-weight: 900; }

/* Materialize renders .badge-notification via badge_notification.css; the
 * radius there was 50% and every usage overrode it inline. */
.badge-notification {
    position: relative;
    color: #fff;
    background-color: var(--accent);
    border-radius: 5px;
    padding: 2px 6px;
    font-size: var(--step--1);
}

/* --- Headings ------------------------------------------------------------
 * Page titles were `grey-text text-lighten-2` (#e0e0e0) -- about 1.3:1 on
 * white. The brand wordmark, the /examples/ tagline and the "Funds with
 * Friends" title were all literally unreadable.
 */
.page-title {
    font-family: var(--font-display);
    font-weight: 700;
    color: var(--ink);
    font-size: var(--step-2);
    text-align: center;
    margin: var(--space-4) 0 var(--space-2);
}
.page-subtitle {
    color: var(--ink-subtle);
    text-align: center;
    margin: 0 auto var(--space-4);
    max-width: var(--measure);
}

/* --- Buttons -------------------------------------------------------------
 * Four different recipes appeared within a few lines of each other on the home
 * screen, including a .btn-flat given a solid fill.
 */
.btn.f-primary, .btn-large.f-primary {
    background-color: var(--brand);
    color: var(--brand-on);
    border-radius: var(--radius-pill);
    text-transform: none;
    font-family: var(--font-display);
}
.btn.f-primary:hover, .btn-large.f-primary:hover { background-color: var(--brand-dark); }

.btn.f-secondary, .btn-large.f-secondary {
    background-color: var(--surface-sunken);
    color: var(--brand-ink);
    border-radius: var(--radius-pill);
    text-transform: none;
    box-shadow: none;
}

.btn-flat.f-quiet {
    color: var(--ink-subtle);
    text-transform: none;
}

/* Destructive, but not the loudest thing on the screen -- "Remove" sits beside
 * two ordinary actions. */
.btn.f-danger-quiet {
    background-color: var(--surface-sunken);
    color: var(--danger-ink);
    border-radius: var(--radius-pill);
    text-transform: none;
    box-shadow: none;
}
.btn.f-danger-quiet:hover { background-color: #FBE9E7; }

/* Destructive and rare: a text button, not a filled one. */
.btn-flat.f-danger-flat { color: var(--danger-ink); text-transform: none; }
.card-leave { margin: var(--space-3) 0 0; text-align: center; }

/* A wrapping row of buttons. Materialize's .btn is inline-block with no margin,
 * so a row of them butts together and, once wrapped, stacks with no gap at all. */
.button-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
}

.btn.f-danger, .btn-large.f-danger {
    background-color: var(--danger);
    color: #fff;
    border-radius: var(--radius-pill);
    text-transform: none;
}

/* Materialize's .disabled is pointer-events only, so a disabled <button> stayed
 * keyboard-submittable. Make the real attribute look disabled too. */
.btn[disabled], .btn-large[disabled], .btn-flat[disabled] {
    opacity: .55;
    cursor: not-allowed;
    pointer-events: none;
}

/* --- Nav ----------------------------------------------------------------
 * Materialize centres .brand-logo.center absolutely (left:50% + translateX).
 * With Account and Log out added, the right-hand list now reaches far enough
 * left to sit underneath the wordmark. On desktop the brand goes to the left
 * where there is room; on mobile the list is hidden and centring is fine.
 */
@media (min-width: 993px) {
    nav .brand-logo.center {
        left: 24px;
        transform: none;
    }
    /* The home screen puts a "help" button in the same corner. Absolute
     * positioning means the wordmark does not push it aside -- it lands on top
     * of it, which is what it did. The selector needs the button to be an
     * *earlier* sibling; see the comment in pulo_base.html. (There used to be a
     * second rule here for the catalog's "Filters" dropdown. The filters are on
     * the page now, so the nav has nothing else in that corner.) */
    nav .nav-help-button ~ .brand-logo.center { left: 116px; }
}

.nav-logout-form { display: inline; }
.nav-logout-button {
    background: none; border: 0; cursor: pointer;
    color: var(--brand-on);
    font: inherit;
    height: 64px; padding: 0 15px;
    display: flex; align-items: center;
}
.nav-logout-button:hover { background-color: rgba(0, 0, 0, .1); }
/* Was a flat link reading a lowercase "help" in the display face, nudged down
 * with `padding-top: 5px` because the icon and the word sat on different
 * baselines. A pill on the bar, aligned by flex, matching the sidenav's log-out
 * button. */
.nav-help-button {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    height: 34px;
    margin: 15px 0 15px 12px;
    padding: 0 .85rem;
    border: 0;
    border-radius: var(--radius-pill);
    background-color: rgba(255, 255, 255, .16);
    color: #fff;
    font-family: var(--font-body);
    font-size: .9rem;
    line-height: 1;
    cursor: pointer;
    float: left;
}
.nav-help-button:hover { background-color: rgba(255, 255, 255, .28); }
.nav-help-button i { font-size: 1.15rem; }
.sidenav-brand { text-align: center; padding: var(--space-4) 0 var(--space-2); }
.sidenav-logout { padding: var(--space-4) 0; text-align: center; }
.sidenav-logout-button {
    background-color: rgba(255, 255, 255, .15);
    color: #fff;
    border-radius: var(--radius-pill);
    text-transform: none;
}

/* Filter pills. The selected one used to carry `disabled`, so it greyed out and
 * read as unavailable while the *unselected* ones read as primary buttons. */
.filter-pill {
    background-color: var(--surface-sunken);
    color: var(--brand-ink);
    border-radius: var(--radius-pill);
    text-transform: none;
    box-shadow: none;
    margin: 0 .15rem;
}
.filter-pill:hover { background-color: #ECE7F6; }
.filter-pill.is-selected {
    background-color: var(--brand);
    color: var(--brand-on);
}
.filter-pill.is-selected:hover { background-color: var(--brand-dark); }

/* --- Cards and surfaces ------------------------------------------------- */
.f-card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-card);
    padding: var(--space-4);
}
.f-prose { max-width: var(--measure); margin-left: auto; margin-right: auto; }

/* The savings figure. Was a 56px btn-floating circle -- a four-digit saving
 * overflowed it -- carrying an infinite `pulse` animation and no hint that it
 * was a link at all. */
.savings-figure {
    display: inline-block;
    padding: .1em .5em;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    color: var(--brand-ink);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-decoration: none;
}
.savings-figure:hover { background: #ECE7F6; }
.savings-figure--potential { color: var(--warning); }

/* --- Money ---------------------------------------------------------------
 * Six different money formats shipped across ten templates. These classes do
 * not fix the formatting -- the templates do -- but they make the *meaning*
 * consistent: owed-to-you is positive, owed-by-you is not.
 */
.f-money { font-variant-numeric: tabular-nums; }
.f-money--positive { color: var(--positive); }
.f-money--negative { color: var(--danger-ink); }
.f-unit { color: var(--ink-subtle); font-size: var(--step--1); }

.outstanding-pill {
    display: inline-block;
    margin-left: var(--space-2);
    padding: .15em .6em;
    border-radius: var(--radius-pill);
    font-size: var(--step--1);
    white-space: nowrap;
}
.outstanding-pill--owed  { background: #E8F5E9; color: #1B5E20; }
.outstanding-pill--owing { background: #FFF3E0; color: #8C3A00; }

/* --- Vendor logos on cards ----------------------------------------------
 * These were sized by an inline `style="width: 40%"` on every card in three
 * templates. 40% of a phone card is about 68px and fits; 40% of a `col l3` card
 * at 1440px is about 135px, which overflowed the 50px-tall slot and spilled the
 * logo out of the top of the card. width stays at 40% so phones render exactly
 * as before -- max-width and max-height are what bind, and only on desktop.
 *
 * The selector must carry `.card .card-image` or Materialize's
 * `.card .card-image img { width: 100% }` (specificity 0,2,1) wins. That is why
 * the sizing was inline in the first place.
 */
.vendor-logo-slot {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 50px;
}
/* The home and examples cards are taller and were on `min-height: 70px`; keep
 * their phone geometry exactly rather than flattening every slot to one number. */
.vendor-logo-slot--tall { height: auto; min-height: 70px; }

/* Phone: exactly the inline rule this replaced -- `width: 40%`, no height cap,
 * no clipping. That geometry was right and must not move. The desktop block
 * below is where the overflow actually happened and is the only place that
 * touches size. */
.card .card-image img.vendor-logo {
    width: 40%;
    height: auto;
    object-fit: contain;
    margin: auto;
}
/* The ten household-bill vendors (Rent, Electric Bill, ...) have no logo file
 * on disk and rendered as a broken image; they get a Material glyph instead.
 * A glyph is decorative, so the name goes alongside it in .sr-only text -- an
 * <img> at least had alt text to lose. */
.vendor-logo-glyph { font-size: 2rem; color: var(--brand); line-height: 1; }
.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;
}

@media (min-width: 993px) {
    /* A desktop card is about twice the width of a phone one, so both the slot
     * and the logo scale with it. max-height is what stops a tall logo spilling
     * out of the top of the card, which is the defect this whole block exists
     * for -- and it is confined here because on a phone nothing overflowed. */
    .vendor-logo-slot { height: 76px; overflow: hidden; }
    .vendor-logo-slot--tall { height: auto; min-height: 88px; }
    .vendor-logo-glyph { font-size: 2.5rem; }
    .card .card-image img.vendor-logo {
        width: 60%;
        max-width: 150px;
        max-height: 100%;
    }
}

/* --- Card tab strips -----------------------------------------------------
 * Materialize's default tab colour is #ee6e73, a salmon red. The catalog cards
 * never overrode it, so every price on /plans/ rendered in the colour the rest
 * of the app uses for errors, with a red underline under the active one. Only
 * /subs/ looked right, because it carries its own page-local override.
 */
.tabs .tab a {
    color: var(--ink-muted);
    font-weight: 600;
}
/* Three prices have to fit across a half-width card on a 390px phone -- about
 * 58px per tab. At Materialize's 14px with 12px of side padding, "$19.99" does
 * not, and the strip became a horizontally scrolling row with the third price
 * cut off at the card edge. */
.card-tabs .tabs .tab a {
    padding: 0 4px;
    font-size: .8rem;
    overflow: visible;
}
.tabs .tab a:hover { color: var(--ink); }
.tabs .tab a.active,
.tabs .tab a:focus.active { color: var(--brand-ink); }
.tabs .tab a:focus { background-color: transparent; }
.tabs .indicator { background-color: var(--brand); }

/* The home screen's category tabs. tabs-fixed-width divides the full width by
 * the tab count, which is right on a phone and leaves three tabs stretched
 * across 72rem on a desktop. */
@media (min-width: 993px) {
    .category-tabs { max-width: 34rem; margin: 0 auto; }
}

/* --- Catalog card body ---------------------------------------------------
 * A tier label, two stats, a button. Materialize's .card-content has 24px of
 * padding and the figures sat in a `.row` (which adds its own margins and a
 * 20px bottom), so a card an inch wide spent most of it on air.
 */
/* 24px of Materialize padding on a card that is 195px wide at 390px leaves
 * about 145px for the content. */
.plan-card-body { padding: var(--space-3) var(--space-2) var(--space-3); }

/* The tier name identifies which tab you are on; it is not what the card is
 * arguing. Bold and near-black it competed with the figures underneath, and on
 * a card that is only about 150px wide that reads as noise. A caption. */
.plan-name {
    margin: 0 0 var(--space-2);
    text-align: center;
    font-weight: 500;
    font-size: var(--step--1);
    color: var(--ink-subtle);
    line-height: 1.3;
}
.card-note { margin: 0 0 var(--space-2); text-align: center; }

.plan-stats {
    display: flex;
    gap: var(--space-2);
    text-align: center;
}
.plan-stat {
    flex: 1 1 0;
    min-width: 0;
    background: var(--surface);
    border-radius: var(--radius-input);
    padding: var(--space-2) var(--space-1);
}
.plan-stat__value {
    display: block;
    font-weight: 700;
    font-size: 1.25rem;
    line-height: 1.2;
    color: var(--brand-ink);
    font-variant-numeric: tabular-nums;
}
.plan-stat__value--muted { color: var(--ink-subtle); }
.plan-stat--highlight .plan-stat__value { color: var(--positive); }
.plan-stat__label {
    display: block;
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--ink-subtle);
}

/* The yearly saving, on the public examples card.
 *
 * It is the single number that page exists to show, and it was set *smaller*
 * than the seat count above it -- so the card led with an incidental figure and
 * buried the argument. Its own tinted band, and the largest thing on the card.
 */
.plan-saving {
    margin: var(--space-2) 0 0;
    padding: var(--space-2) var(--space-1);
    border-radius: var(--radius-input);
    background: #E8F5E9;
    text-align: center;
}
.plan-saving__value {
    display: block;
    font-weight: 700;
    font-size: 1.5rem;
    line-height: 1.15;
    color: #1B5E20;
    font-variant-numeric: tabular-nums;
}
.plan-saving__label {
    display: block;
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: #2E7D32;
}

.card-add { margin: var(--space-3) 0 0; }
.card-add__button { width: 100%; }
/* A state, not a control: same footprint so the grid does not jump, but it does
 * not invite a tap. */
.card-add__button--static {
    background: var(--surface-sunken);
    border-radius: var(--radius-pill);
    cursor: default;
}

/* Subscription-card figures. Every number on the card used to be labelled by a
 * tooltip and nothing else -- and the tooltips hang off <div>s, so they were
 * keyboard-unreachable and needed a tap on touch. The price also carried no
 * period at all. */
.card-prices { margin-bottom: 0; }
/* `col s6` is exactly half the card, and half of a 195px card on a 390px phone
 * is about 62px -- less than "FULL PRICE" needs, so it broke onto two lines
 * while "YOURS" beside it used a third of its column. The pair fits easily; it
 * was the fixed 50/50 split that did not. Let each side take the width of its
 * own content and push them to the edges.
 *
 * min-width: 0 so an unexpectedly long label still wraps rather than pushing
 * its neighbour out of the card. */
.card-prices {
    display: flex;
    justify-content: space-between;
    gap: var(--space-2);
}
.card-prices > .col {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
    padding: 0;
}
.card-prices .f-unit { font-size: .7rem; text-transform: uppercase; letter-spacing: .02em; }
.was-price {
    color: var(--ink-subtle);
    text-decoration: line-through;
    font-variant-numeric: tabular-nums;
}
.now-price {
    color: var(--brand-ink);
    font-weight: 700;
    font-size: 1.1rem;
    font-variant-numeric: tabular-nums;
}
.could-price {
    color: var(--positive);
    font-weight: 700;
    font-size: 1.1rem;
    font-variant-numeric: tabular-nums;
}
.card-stat {
    display: inline-block;
    padding: .1em .55em;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    color: var(--ink-muted);
    font-size: var(--step--1);
    text-decoration: none;
}

/* --- The /join/ card body ------------------------------------------------
 * Four things stacked with no consistent gap between them: the owner, the two
 * prices, the seat count, the button. Materialize's 24px card padding made the
 * card feel tight at the edges while the elements inside ran together.
 */
.join-card-body { padding: var(--space-3) var(--space-3) var(--space-4); }
.join-card-body .card-prices { margin-top: var(--space-3); }
.join-mutual {
    margin-top: var(--space-3);
    text-align: center;
}
.join-mutual a { font-size: var(--step--1); }

/* A seat meter: one pip per seat, filled where somebody is sitting. */
.seat-meter {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-3) 0 0;
    flex-wrap: wrap;
}
.seat-meter__pips { display: flex; gap: 3px; }
.seat-pip {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    border: 1.5px solid var(--brand);
    box-sizing: border-box;
}
.seat-pip--taken { background: var(--brand); }
.seat-meter__label { color: var(--ink-subtle); font-size: var(--step--1); }

.join-owner__note {
    font-weight: 400;
    font-size: var(--step--1);
    color: var(--ink-subtle);
}
.join-owner {
    margin: 0 0 var(--space-3);
    line-height: 1.3;
    overflow-wrap: anywhere;
    font-weight: 700;
    color: var(--ink);
}

/* Card tabs: icon over a short label, rather than a glyph whose only name was
 * a tooltip -- unreachable by keyboard, and needing a tap on touch. */
.card-tab-strip { height: 3.4rem; }
.card-tab-strip .tab a {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    line-height: 1.1;
    padding: 0;
}
.card-tab-strip .tab a i { font-size: 1.25rem; }
.card-tab-label {
    font-size: .65rem;
    text-transform: uppercase;
    letter-spacing: .04em;
}

/* --- Dialogs -------------------------------------------------------------
 * One shape for every modal in the app. They were all different: 50px corners
 * on the landing page, 30px on the join dialogs, 2px (Materialize's default)
 * elsewhere; footers that were sometimes a .modal-footer and sometimes a bare
 * floated <a> outside the content; close controls labelled "Sounds good",
 * "Close", "Stay", or nothing at all.
 *
 * The purple keyline is what makes a dialog read as a dialog rather than as a
 * white sheet on a white page -- Materialize gives the overlay only a 50%
 * black wash, which on a light page is easy to miss.
 */
.f-modal {
    border: 2px solid var(--brand);
    border-radius: var(--radius-card);
    max-width: 34rem;
    /* Dialogs are rendered where they are triggered, so several sit inside
     * centred cards and inherited the centring. Prose reads left. */
    text-align: left;
}
.f-modal .modal-content { padding: var(--space-5) var(--space-4) var(--space-4); }
.f-modal__eyebrow {
    margin: 0;
    font-size: var(--step--1);
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--brand-ink);
    font-weight: 600;
}
.f-modal__title {
    margin: var(--space-1) 0 var(--space-3);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--step-2);
    line-height: 1.2;
    color: var(--ink);
}
.f-modal__body { color: var(--ink-muted); line-height: 1.5; }
/* `.modal .modal-footer` in Materialize is (0,2,0) and pins height: 56px with
 * 6px of side padding -- not enough for a real button. */
.f-modal .modal-footer {
    border-radius: 0 0 calc(var(--radius-card) - 2px) calc(var(--radius-card) - 2px);
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    justify-content: flex-end;
    align-items: center;
    padding: var(--space-3);
    height: auto;
}

/* --- Back --------------------------------------------------------------
 * There were four of these: a grey square with one rounded side, a purple pill
 * with the arrow on the wrong side of the word, a flat grey link, and a plain
 * text link. Two of them called history.back(), which is not a place -- it
 * takes you to wherever you happened to come from, including back into a form
 * you just submitted.
 */
.f-back {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .4rem .9rem .4rem .6rem;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    color: var(--ink-muted);
    font-size: var(--step--1);
    text-transform: none;
    line-height: 1;
}
.f-back:hover { background: #ECE7F6; color: var(--brand-ink); }
.f-back i { font-size: 1.1rem; }
.f-back-bar { padding: var(--space-3) 0 0; }

/* --- Filter bar ----------------------------------------------------------
 * The category filter used to be a Materialize dropdown hung off the navbar,
 * behind a word ("Filters") that gave no clue what the options were or which
 * one was active. On the page, as pills, the current filter is visible and the
 * rest are one tap away. Scrolls sideways on a phone rather than wrapping to
 * four rows of chips.
 */
/* A <nav> for semantics -- these are the page's navigation between views of the
 * same list -- which means undoing what Materialize does to the element: it
 * styles bare `nav` as the app bar, salmon background and all. */
nav.filter-bar {
    background-color: transparent;
    box-shadow: none;
    height: auto;
    line-height: normal;
    width: auto;
    color: inherit;
}
.filter-bar {
    display: flex;
    gap: var(--space-2);
    overflow-x: auto;
    padding: var(--space-1) var(--space-1) var(--space-3);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
.filter-bar::-webkit-scrollbar { display: none; }
.filter-bar .filter-pill { flex: 0 0 auto; margin: 0; }
.filter-bar__label {
    align-self: center;
    flex: 0 0 auto;
    margin-right: var(--space-1);
    font-size: var(--step--1);
    color: var(--ink-subtle);
    text-transform: uppercase;
    letter-spacing: .04em;
}
@media (min-width: 993px) {
    /* Room to wrap on a desktop, where a sideways-scrolling strip is a worse
     * affordance than simply showing everything. */
    .filter-bar { flex-wrap: wrap; overflow-x: visible; }
}
.filter-pill i { font-size: 1.05rem; vertical-align: -.18em; margin-right: .15rem; }

/* --- The help dialog -----------------------------------------------------
 * Numbered steps rather than a bulleted list of features: the question a new
 * user has is "what do I do", and three numbers answer it in the order they
 * happen. It replaced a three-step coach-mark tour -- see the note in
 * subs_list.html for why that had to go rather than be fixed.
 */
/* People, with faces. Used in the mutual-friends dialog, where a list of bare
 * first names was being offered as grounds for trusting a stranger. */
.people-group__label {
    margin: var(--space-3) 0 var(--space-2);
    font-size: var(--step--1);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--ink-subtle);
}
.people-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.person-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: .25rem .85rem .25rem .25rem;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
}
.person-chip__photo {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    object-fit: cover;
    flex: 0 0 auto;
}
.person-chip__name { font-weight: 600; color: var(--ink); }

/* The landing dialog's price comparison. An abstract claim ("save 50-90%") does
 * not land the way one real subscription with two numbers under it does. */
.pitch-figure {
    margin: var(--space-4) 0;
    padding: var(--space-3);
    border-radius: var(--radius-input);
    background: var(--surface-sunken);
}
.pitch-figure__row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-1) 0;
}
.pitch-figure__label { color: var(--ink-muted); }
.pitch-figure__was {
    color: var(--ink-subtle);
    text-decoration: line-through;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.pitch-figure__now {
    color: var(--positive);
    font-weight: 700;
    font-size: var(--step-1);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.pitch-figure__note {
    margin: var(--space-2) 0 0;
    color: var(--brand-ink);
    font-weight: 600;
}

/* The numbered steps are the only thing the help dialog adds to .f-modal. */
.help-steps { list-style: none; margin: 0; padding: 0; }
.help-step {
    display: flex;
    gap: var(--space-3);
    padding: 0 0 var(--space-4);
}
.help-step:last-child { padding-bottom: 0; }
.help-step__num {
    flex: 0 0 2rem;
    height: 2rem;
    border-radius: 50%;
    background: var(--brand);
    color: var(--brand-on);
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}
.help-step__body { color: var(--ink-muted); line-height: 1.45; }
.help-step__head {
    display: block;
    font-weight: 700;
    color: var(--ink);
}
.f-modal__aside {
    margin: var(--space-4) 0 0;
    padding: var(--space-3);
    border-radius: var(--radius-input);
    background: var(--surface-sunken);
    color: var(--ink-muted);
}

/* --- The home screen's Add / Join tile ------------------------------------
 * A grid cell like any other now, so it stretches to the height of the cards
 * beside it. The artwork takes the slack between the two buttons; it used to be
 * a fixed 120px image with <br><br> above it, which is why the tile came up
 * short next to a card on desktop.
 */
.add-tile { overflow: hidden; }
/* The two buttons cap the tile, so only their outer corners are rounded -- the
 * inner ones met the artwork in the middle with a stray curve on each. */
.add-tile__btn { flex: 0 0 auto; width: 100%; }
.add-tile__btn--join { border-radius: var(--radius-card) var(--radius-card) 0 0; }
.add-tile__btn--add { border-radius: 0 0 var(--radius-card) var(--radius-card); }

/* A grid cell hidden by the category filter. [hidden] alone loses to the
 * `display: flex` the grid puts on its columns. */
.card-grid > .col[hidden] { display: none; }
.add-tile__art {
    flex: 1 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-3);
    min-height: 96px;
}
.add-tile__art img { max-height: 120px; max-width: 60%; height: auto; }

/* --- Page sections ------------------------------------------------------
 * The friends page is a sequence of sections rather than one search box with
 * everything else hidden in modals, so it needs a heading level between
 * .page-title and body copy.
 */
.section-title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--step-1);
    color: var(--ink);
    margin: var(--space-5) 0 var(--space-3);
}
.count-pill {
    display: inline-block;
    min-width: 1.6em;
    padding: .1em .5em;
    margin-left: .35em;
    border-radius: var(--radius-pill);
    background: var(--brand);
    color: var(--brand-on);
    font-family: var(--font-body);
    font-size: var(--step--1);
    font-weight: 600;
    text-align: center;
    vertical-align: middle;
}

/* Empty states. Every one of these used to be either nothing at all or a
 * disabled-looking button that did not say why. */
.f-empty {
    background: var(--surface-sunken);
    border-radius: var(--radius-card);
    padding: var(--space-4);
    text-align: center;
}
.f-empty p { margin: 0 0 var(--space-1); }
.f-empty p:last-child { margin-bottom: 0; }

/* --- The install prompt --------------------------------------------------
 * Not a modal on purpose: a modal gets dismissed reflexively and cannot be
 * returned to. This sits in the page above the subscriptions, where somebody
 * who has just noticed they are owed money will read it.
 */
.install-prompt { padding-top: var(--space-3); }
.install-card {
    max-width: 34rem;
    margin: 0 auto;
    padding: var(--space-4);
    border: 2px solid var(--brand);
    border-radius: var(--radius-card);
    background: var(--surface);
}
.install-card__head { display: flex; gap: var(--space-3); align-items: flex-start; }
.install-card__icon {
    flex: 0 0 auto;
    width: 42px;
    height: 42px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--brand);
    color: var(--brand-on);
}
.install-card__icon--done { background: var(--positive); }
.install-card__title {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--step-1);
    line-height: 1.25;
    color: var(--ink);
}
.install-card__sub { margin: var(--space-1) 0 0; color: var(--ink-muted); }
.install-steps { margin-top: var(--space-3); }
.install-steps__intro { margin: 0 0 var(--space-2); color: var(--ink-muted); }
.install-steps__list { list-style: none; margin: 0; padding: 0; }
.install-steps__list li {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .35rem;
    padding: var(--space-2) 0;
    color: var(--ink-muted);
}
.install-steps__num {
    flex: 0 0 auto;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: 50%;
    background: var(--surface-sunken);
    color: var(--brand-ink);
    font-weight: 700;
    font-size: var(--step--1);
    display: flex;
    align-items: center;
    justify-content: center;
}
/* The controls are named as they appear on the device, so the sentence matches
 * what the reader is looking at. */
.install-steps__key {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    padding: .1rem .5rem;
    border-radius: var(--radius-input);
    background: var(--surface-sunken);
    color: var(--ink);
    font-weight: 600;
    white-space: nowrap;
}
.install-steps__key i { font-size: 1.05rem; }
.install-card__dismiss { margin-top: var(--space-2); padding-left: 0; }

/* --- The edit form -------------------------------------------------------
 * Sections, so a long form reads as four short ones.
 */
.edit-form { max-width: 42rem; }
.edit-card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-card);
    padding: var(--space-3) var(--space-4) var(--space-4);
    margin-bottom: var(--space-4);
}
.edit-card__title {
    margin: 0 0 var(--space-2);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--step-1);
    color: var(--ink);
}
.edit-card__hint {
    margin: var(--space-3) 0;
    padding: var(--space-3);
    border-radius: var(--radius-input);
    background: var(--surface-sunken);
    color: var(--ink-muted);
}
.edit-card__gain { color: var(--positive); font-weight: 700; }
.edit-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: center;
    justify-content: space-between;
    padding-bottom: var(--space-4);
}

/* --- Alerts --------------------------------------------------------------
 * Every row used to look the same whether it arrived a minute or a month ago.
 */
.alert-list .collection-item.alert-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3);
    border-left: 3px solid transparent;
}
.alert-row--unseen {
    background: #F6F2FD;
    border-left-color: var(--brand);
}
.alert-row__icon {
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--surface-sunken);
    color: var(--ink-muted);
}
.alert-row__icon i { font-size: 1.15rem; }
/* Money is the category that costs you something to miss. */
.alert-row__icon--3 { background: #E8F5E9; color: var(--positive); }
.alert-row__icon--0 { background: #FFF3E0; color: var(--warning); }
.alert-row__body { flex: 1 1 auto; min-width: 0; }
.alert-row__text { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
.alert-row__meta {
    margin: var(--space-1) 0 0;
    font-size: var(--step--1);
    color: var(--ink-subtle);
}
.alert-row__dot {
    flex: 0 0 auto;
    width: 9px;
    height: 9px;
    margin-top: .5rem;
    border-radius: 50%;
    background: var(--brand);
}
.pagination-position { padding: 0 var(--space-2); color: var(--ink-muted); }

.f-collection { border-radius: var(--radius-card); overflow: hidden; }
/* Materialize pins .secondary-content to the top-right corner, which puts a
 * pair of buttons half outside a 42px avatar row. */
.f-collection .secondary-content.person-actions {
    position: static;
    float: right;
    display: flex;
    gap: var(--space-2);
    align-items: center;
}

.friend-search__submit { padding-top: var(--space-3); }

/* The friends list folds away. The default disclosure triangle is removed in
 * favour of a chevron that rotates, which needs the WebKit pseudo-element too --
 * Safari still paints the marker without it. */
.friends-panel__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    cursor: pointer;
    list-style: none;
}
.friends-panel__summary::-webkit-details-marker { display: none; }
.friends-panel__summary:hover .friends-panel__title { color: var(--brand-ink); }
.friends-panel__title { margin-bottom: var(--space-3); }
.friends-panel__chevron {
    color: var(--ink-subtle);
    transition: transform .18s ease;
}
.friends-panel[open] .friends-panel__chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
    .friends-panel__chevron { transition: none; }
}

.friend-tile {
    text-align: center;
    padding: var(--space-3) var(--space-2);
}
.friend-tile__photo {
    width: 72px; height: 72px;
    object-fit: cover;
    display: block;
    margin: 0 auto var(--space-2);
}
.friend-tile__name {
    margin: 0;
    font-weight: 600;
    color: var(--ink);
    overflow-wrap: anywhere;
}

.invite-panel {
    background: var(--surface-sunken);
    border-radius: var(--radius-card);
    padding: var(--space-3) var(--space-4) var(--space-4);
    max-width: 34rem;
}
/* Vertically centred on the field beside it, which carries Materialize's own
 * 1rem top margin. */
.invite-panel__add { padding-top: var(--space-4); }
/* .f-secondary is a --surface-sunken button, and so is this panel, so the Add
 * button disappeared into its own background and read as a text link. */
.invite-panel .btn.f-secondary { background-color: var(--surface); }
.invite-panel .btn.f-secondary:hover { background-color: #ECE7F6; }
/* Materialize gives .chips a bottom border and a 45px min-height whether or not
 * it holds anything, which drew a rule across an empty panel. */
.invite-panel .chips { min-height: 0; margin-bottom: var(--space-2); border-bottom: 0; }

.catalog-search {
    max-width: 26rem;
    margin: 0 auto var(--space-3);
}

/* --- Forms -------------------------------------------------------------- */
/* --- Form fields ---------------------------------------------------------
 * Materialize's accent is a teal, #26a69a, and it is what every focused input
 * in the app was drawing: teal underline, teal floating label, teal prefix
 * icon. On the signup screen that made the one thing the user is looking at
 * the only non-brand colour on the page. Same defect as the teal button on
 * /join/'s empty state.
 *
 * Matching Materialize's own specificity (:not(.browser-default):focus
 * :not([readonly])) so this wins without !important.
 */
input:not([type]):not(.browser-default):focus:not([readonly]),
input[type=text]:not(.browser-default):focus:not([readonly]),
input[type=password]:not(.browser-default):focus:not([readonly]),
input[type=email]:not(.browser-default):focus:not([readonly]),
input[type=url]:not(.browser-default):focus:not([readonly]),
input[type=tel]:not(.browser-default):focus:not([readonly]),
input[type=number]:not(.browser-default):focus:not([readonly]),
input[type=search]:not(.browser-default):focus:not([readonly]),
textarea.materialize-textarea:focus:not([readonly]) {
    border-bottom: 1px solid var(--brand);
    box-shadow: 0 1px 0 0 var(--brand);
}
input:not([type]):not(.browser-default):focus:not([readonly]) + label,
input[type=text]:not(.browser-default):focus:not([readonly]) + label,
input[type=password]:not(.browser-default):focus:not([readonly]) + label,
input[type=email]:not(.browser-default):focus:not([readonly]) + label,
input[type=url]:not(.browser-default):focus:not([readonly]) + label,
input[type=tel]:not(.browser-default):focus:not([readonly]) + label,
input[type=number]:not(.browser-default):focus:not([readonly]) + label,
input[type=search]:not(.browser-default):focus:not([readonly]) + label,
textarea.materialize-textarea:focus:not([readonly]) + label {
    color: var(--brand-ink);
}
.input-field .prefix.active { color: var(--brand); }
/* Radios and checkboxes carry the same teal. */
[type="radio"]:checked + span:not(.lever)::after,
[type="radio"].with-gap:checked + span:not(.lever)::before,
[type="radio"].with-gap:checked + span:not(.lever)::after {
    border-color: var(--brand);
}
[type="radio"]:checked + span:not(.lever)::after,
[type="radio"].with-gap:checked + span:not(.lever)::after {
    background-color: var(--brand);
}
[type="checkbox"]:checked + span:not(.lever)::before {
    border-right-color: var(--brand);
    border-bottom-color: var(--brand);
}
.chips.focus { border-bottom: 1px solid var(--brand); box-shadow: 0 1px 0 0 var(--brand); }
.dropdown-content li > a, .dropdown-content li > span { color: var(--brand-ink); }

.f-field-error { color: var(--danger-ink); font-size: var(--step--1); display: block; }
.f-help { color: var(--ink-subtle); font-size: var(--step--1); }

/* --- Focus ---------------------------------------------------------------
 * Materialize 1.0.0 gives .btn no visible focus ring at all, and nothing in
 * this project defined one.
 */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible,
.collapsible-header:focus-visible {
    outline: 3px solid var(--brand-ink);
    outline-offset: 2px;
    border-radius: 4px;
}

/* --- Desktop -------------------------------------------------------------
 * Every signed-in page rendered in the left ~460px of the window, because
 * Materialize's .container is 70% wide but the card grids inside it were sized
 * for a phone. Give the content a real column and let the grids breathe.
 */
main.content { padding-bottom: var(--space-5); }

/* The card grids are bare `<div class="row">` elements sitting directly under
 * main, not inside a .container -- so the headings above them were clamped to
 * 72rem while the grid itself ran the full width of the window. That mismatch
 * was most of what made /plans/ look wrong on a desktop.
 *
 * The wrapper is `.grid-wrap`, NOT `.container`, and that is the whole point:
 * Materialize's .container is `width: 90%` on a phone, so wrapping the grids in
 * one shaved 10% off every card. At 390px that took a `col s6` card from 195px
 * to 175px -- enough to shrink every vendor logo and to wrap "FULL PRICE" and
 * "YOU WOULD PAY" onto two lines. A phone has no width to spare; the clamp is a
 * desktop-only concern and now lives in a desktop-only rule.
 */
.grid-wrap { width: 100%; }

/* --- The card grid itself ------------------------------------------------
 * Materialize's grid floats its columns, and floats catch on each other. One
 * card taller than its neighbours -- a /join/ card carrying "3 mutual friends",
 * say -- and the next card does not return to the left edge, it tucks under the
 * tallest thing above it. On /join/ that put the fifth card in the fourth
 * column of the second row with a hole beside it. No amount of per-card tuning
 * fixes that; the layout model is wrong for a grid of unknown-height cards.
 *
 * Flex wrapping has no such behaviour: every line starts at the left edge, and
 * `align-items: stretch` (the default) makes every card on a line as tall as
 * the tallest. That second part fixes the ADD/JOIN tile too, which was a little
 * shorter than the cards beside it on desktop -- it now stretches to match,
 * without touching the phone, where it already happened to line up.
 *
 * The .col percentage widths still apply, so the breakpoints are unchanged.
 * `float` is ignored on a flex item. The ::after clearfix has to go, or it
 * becomes a flex item of its own and lands as a phantom cell.
 */
.card-grid {
    display: flex;
    flex-wrap: wrap;
}
.card-grid::after { display: none; }
.card-grid > .col { display: flex; }
/* Materialize sets `margin-left: auto` on every `.row .col.sN` -- part of how
 * its offset classes work. Under floats that resolves to 0 and does nothing.
 * Under flex an auto margin absorbs the free space on its line, so a last line
 * holding two of four columns had both cards shoved to the right, leaving gaps
 * in slots one and three -- which looked exactly like the float bug this rule
 * set exists to fix.
 *
 * Written as `.row.card-grid > .col` to match the specificity of Materialize's
 * `.row .col.s6` (0,3,0); funess.css is loaded after it, so the tie falls here. */
.row.card-grid > .col {
    margin-left: 0;
    margin-right: 0;
}
/* The card is the flex item's only child, so it takes the stretched height. */
.card-grid > .col > .card {
    display: flex;
    flex-direction: column;
    width: 100%;
    margin-top: .5rem;
    margin-bottom: .5rem;
}
/* Let the body take the slack, so the action sits on the bottom edge of every
 * card in a line however much copy is above it. */
.card-grid > .col > .card > .card-content { flex: 1 0 auto; }
.card-grid > .col > .card .card-add { margin-top: auto; }

@media (min-width: 993px) {
    main.content > .container,
    main.content > .row,
    .grid-wrap {
        max-width: var(--page-max);
        width: 90%;
        margin-left: auto;
        margin-right: auto;
    }
}

/* Respect the setting rather than animating regardless. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
    }
}
