   
/* S-Presso Group Dashboard Styles */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600&family=Noto+Sans:wght@400;500;600&family=Noto+Sans+JP&display=swap');

/* BASECSS_ADOPT_V1 - analytics no longer defines a palette. base.css owns the
   design system (--navy-*, --gold-*, --gray-*, --font-sans, --font-display) and
   is now loaded first by analytics/index.html. This block only ALIASES those
   tokens and adds the few base.css genuinely lacks. If a colour needs changing,
   change it in base.css so every page moves together - that is the entire point.

   Aliases: analytics' own rules read --navy/--gold, so one line maps them to the
   system rather than 30 rules hardcoding hexes.

   Real gaps in base.css, added here for now (candidates to promote upstream):
   - on-tint inks. base.css has --success/--error and --success-bg/--error-bg but
     no ink to sit ON the tint. Paired directly they FAIL: #10b981 on #d1fae5 is
     2.24:1, #dc2626 on #fee2e2 is 3.95:1. These -fg values are the missing half.
   - --cream: the site's page colour (#faf7f0). base.css only has cold --gray-100. */
:root {
    --navy: var(--navy-900);
    --gold: var(--gold-600);
    --text: var(--navy-900);
    --muted: var(--gray-500);
    --cream: #faf7f0;
    --hairline: #e8e0cf;
    /* STATUS_FG_V1 - the inks now come from base.css (--success-fg/--error-fg/--warning-fg),
       promoted there so every page gets them. Only the pill TINTS stay local: analytics wants
       them slightly warmer than base.css's --success-bg/--error-bg to sit on cream rather
       than on the cold grey the other pages use. Tints are decoration; the inks are the part
       that has to be right, and those are shared. */
    --up-bg: #dcf0e0;
    --up-fg: var(--success-fg);
    --down-bg: #f9dedb;
    --down-fg: var(--error-fg);
    --warn-fg: var(--warning-fg);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Noto Sans', sans-serif;
    background: var(--cream);
    color: var(--text);
    min-height: 100vh;
    line-height: 1.6;
    padding: 20px;
    max-width: 98%;
    margin: 0 auto;
    min-width: 320px;
}

/* Main Header */
/* AN_TOPBAR_V3 - full-width two-tone (navy->gold) top bar */
/* BRAND_TOKENS_V1 - flat navy + gold hairline. The site never fades navy into
   gold; gold is an accent there, so it is a 2px rule here, not half the bar. */
.an-topbar {
  position: sticky; top: 0; z-index: 50;
  background: var(--navy);
  border-bottom: 2px solid var(--gold);
  height: 60px;
  display: flex; align-items: center;
  padding: 0 20px; gap: 14px;
  margin: 0 0 24px;
  box-shadow: 0 2px 12px rgba(15, 31, 61,0.18);
}
.an-topbar .an-logo-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.an-topbar .an-logo-wrap img { height: 40px; width: auto; display: block; }
.an-topbar .an-brand { font-family: var(--font-display); font-size: 1.5rem; font-weight: 600; color: #fff; white-space: nowrap; text-shadow: 0 1px 3px rgba(0,0,0,0.25); }
/* GOLD_SCALE_V1 - --gold-500 (light) NOT --gold-600 here, and that is deliberate: this word
   sits on the navy bar, where gold-500 is 6.76:1 and gold-600 only 4.13:1. The two golds are
   not interchangeable - 600 is for gold ON light, 500 is for gold ON dark. */
.an-topbar .an-brand .an-accent { color: var(--gold-500); }
.an-topbar .an-right { display: flex; align-items: center; gap: 14px; margin-left: auto; }
.an-topbar .an-user { font-size: 0.85rem; font-weight: 700; color: #fff; white-space: nowrap; text-shadow: 0 1px 2px rgba(0,0,0,0.2); }
.an-topbar .an-icon-btn {
  width: 34px; height: 34px; border-radius: 8px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.4);
  color: #fff; cursor: pointer; transition: all .15s ease;
}
.an-topbar .an-icon-btn:hover { background: rgba(255,255,255,0.95); border-color: #fff; color: var(--navy); }
.an-topbar .an-icon-btn svg { width: 17px; height: 17px; stroke-width: 2; }
.an-topbar .an-langbtn {
  height: 34px; min-width: 40px; padding: 0 11px; border-radius: 8px;
  background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.4);
  color: #fff; cursor: pointer; font-size: 0.8rem; font-weight: 700;
  letter-spacing: .5px; font-family: inherit; transition: all .15s ease;
}
.an-topbar .an-langbtn:hover { background: rgba(255,255,255,0.95); border-color: #fff; color: var(--navy); }
.an-topbar .an-logout {
  display: flex; align-items: center; gap: 6px;
  padding: 7px 16px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.25);
  background: rgba(220,38,38,0.92); color: #fff;
  font-size: 0.78rem; font-weight: 700; font-family: inherit;
  cursor: pointer; transition: all .15s ease;
}
.an-topbar .an-logout:hover { background: var(--error); border-color: rgba(255,255,255,0.5); }

/* GAS_DEADCODE_V1 - the entire Apps Script header block removed here:
   .main-header(::before) .header-container .brand-section .logo(+img)
   .hotel-title(::after) .hotel-subtitle .language-toggle(:hover) and .bg-teal.
   That header was replaced by .an-topbar (AN_TOPBAR_V2) and the markup was deleted,
   but ~100 lines of its CSS stayed behind - including two more navy->gold gradients
   and a teal card colour used nowhere. Verified dead before removal: zero class
   references in analytics/index.html and analytics.js. (analytics.js:1832 still
   queries .hotel-title but is null-guarded, so it no-ops safely.) */

/* Navigation Tabs */
/* AN_TABS_BOXED_V2 - outlined rounded boxes (marketing look) on light bg */
.tabs {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: center;
    max-width: 1200px;
    margin: 0 auto 30px;
    padding: 0 20px;
}

.tab {
    padding: 10px 20px;
    border: 1px solid rgba(15, 31, 61,0.2);
    border-radius: 8px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.92rem;
    font-family: 'Noto Sans', sans-serif;
    color: var(--navy);
    background: #fff;
    box-shadow: 0 1px 3px rgba(15, 31, 61,0.08);
    transition: all 0.2s ease;
}

.tab:hover {
    border-color: var(--gold);
    color: var(--navy);
    background: #fff;
    box-shadow: 0 2px 8px rgba(15, 31, 61,0.12);
}

.tab.active {
    /* AN_TAB_ACTIVE_BLUE_V1 */
    background: rgba(15, 31, 61,0.85);
    color: #fff;
    border-color: rgba(255,255,255,0.4);
    box-shadow: 0 4px 14px rgba(15, 31, 61,0.25);
}

/* Section Styling */
/* FONT_DASHBOARD_V1 - was Lora, briefly Playfair; both are display serifs and
   neither belongs on a data screen. base.css already settled this: --font-sans
   for reading (10 uses), --font-display reserved for brand moments (2 uses).
   checkin/roomboard/pricing all read in --font-sans. Analytics now matches. */
h2, h3 {
    color: var(--navy);
    font-family: var(--font-sans);
    font-weight: 600;
    margin-bottom: 25px;
    font-size: 1.55rem;
    letter-spacing: 0;
}

.section {
    margin-bottom: 40px;
    background: white;
    padding: 30px;
    border-radius: 16px;
    box-shadow: 0 8px 25px rgba(15, 31, 61, 0.1);
    border: 1px solid rgba(15, 31, 61, 0.1);
    position: relative;
    overflow: hidden;
    width: 100%;
    box-sizing: border-box;
}

/* FLATTEN_CHROME_V1 - one brand gesture, repeated: a 2px gold rule. Same as the
   topbar underline and the emphasis card. The navy->gold fade said nothing and
   fought the flat cards below it. */
.section::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: var(--gold);
    border-radius: 16px 16px 0 0;
}

/* Metrics Cards */
.metrics-container {
    margin-bottom: 40px;
}

.metrics-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 24px;
    margin-bottom: 40px;
}

/* BRAND_TOKENS_V1 - one surface for every card. Colour now only appears where
   it means something (the trend pill). Hover tints the border gold, which is
   literally the site's ocean_hover_primary_color. No lift: cards aren't links. */
.metric-card {
    padding: 20px;
    border-radius: 12px;
    background: var(--white);
    border: 1px solid var(--hairline);
    color: var(--navy);
    display: flex;
    flex-direction: column;
    gap: 6px;
    position: relative;
    overflow: hidden;
    transition: border-color 0.2s ease;
}

.metric-card:hover {
    border-color: var(--gold);
}

.metric-card::before {
    content: none;
}

/* BRAND_TOKENS_V1 - explicit muted ink, not opacity. Opacity was compensating
   for white-on-gradient; on a white card it just greys the text out. */
.metric-title {
    font-size: 0.95rem;
    margin-bottom: 6px;
    font-weight: 500;
    color: var(--muted);
    z-index: 2;
    position: relative;
    font-family: 'Noto Sans', sans-serif;
}

/* BRAND_TOKENS_V1 - figures are data, not display type: sans + tabular so
   columns of yen line up. Playfair is for headings only, as on the site. */
.metric-value {
    font-size: 2rem;
    font-weight: 600;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    z-index: 2;
    position: relative;
    color: var(--navy);
    font-family: 'Noto Sans', sans-serif;
    font-variant-numeric: tabular-nums lining-nums;
    letter-spacing: -0.02em;
}

.metric-subtitle {
    font-size: 0.85rem;
    color: var(--muted);
    margin-top: 2px;
    z-index: 2;
    position: relative;
    font-variant-numeric: tabular-nums;
}

/* BRAND_TOKENS_V1 - the deltas are the only saturated colour on the row now.
   Tinted pill + dark ink reads >7:1 on the white card; the old bare green/red
   scored as low as 1.21:1 because it inherited whatever gradient sat behind it. */
.trend-up,
.trend-down {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 0.8rem;
    font-weight: 600;
    padding: 2px 8px;
    border-radius: 99px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.trend-up {
    background: var(--up-bg);
    color: var(--up-fg);
}

.trend-down {
    background: var(--down-bg);
    color: var(--down-fg);
}

/* S-Presso Group Color Scheme */
/* BRAND_TOKENS_V1 - the four names stay: the HTML hardcodes them and updateMetrics()
   emits them, so removing them would mean touching JS. Only what they DO changed.

   CARD_UNIFORM_V1 - all four are now IDENTICAL. bg-gold briefly kept a 2px gold top rule as
   an "emphasis" variant, but which card wears it is decided by slot, not by meaning: it
   landed on Next Month Sales, Net Revenue, Market Average and "¥400 Tax Guests" - four cards
   with nothing in common except their position. That is the same mistake as the original
   colour rotation, just quieter, and it read as one card being oddly outlined. A row of
   cards should look like a row of cards. If a metric ever needs emphasis, it should earn it
   from its VALUE at render time, not from its index. */
.bg-blue,
.bg-slate,
.bg-navy,
.bg-gold {
    background: var(--white);
}

/* Filter Controls */
.filter-controls {
    display: flex;
    gap: 15px;
    margin-bottom: 25px;
    flex-wrap: wrap;
    align-items: center;
}

.filter-controls select {
    padding: 10px 15px;
    border: 1px solid rgba(15, 31, 61, 0.15);
    border-radius: 10px;
    background: white;
    color: var(--navy);
    font-family: 'Noto Sans', sans-serif;
    font-size: 0.95rem;
    transition: all 0.3s ease;
    min-width: 140px;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(15, 31, 61, 0.08);
}

/* HOVER_GOLD_V1 - mirrors .top-bar-refresh:hover / .filter-btn:hover in base.css */
.filter-controls select:hover {
    border-color: var(--gold-600);
    box-shadow: 0 4px 12px rgba(160, 122, 34, 0.25);
    transform: translateY(-1px);
}

.filter-controls select:focus {
    outline: none;
    border-color: var(--gold);
    box-shadow: 0 0 0 3px rgba(160, 122, 34, 0.15), 0 4px 12px rgba(15, 31, 61, 0.12);
}

.filter-controls select:active {
    transform: translateY(0);
}

/* Tables */
.table-container {
    width: 100%;
    margin: 30px 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border-radius: 12px;
    box-shadow: 0 4px 15px rgba(15, 31, 61, 0.1);
    background: white;
}

.data-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: 0.9rem;
    font-family: 'Noto Sans', sans-serif;
}

.data-table th,
.data-table td {
    padding: 12px 15px;
    text-align: left;
    border-bottom: 1px solid rgba(15, 31, 61, 0.1);
    white-space: nowrap;
}

/* FLATTEN_CHROME_V1 - solid navy. White on --navy-900 is 16.36:1; the gradient's
   light end (#2d5a87) dropped it to ~7:1 for no gain. */
.data-table th {
    background: var(--navy);
    color: white;
    font-weight: 600;
    position: sticky;
    top: 0;
    z-index: 10;
    font-family: var(--font-sans);
    letter-spacing: 0.3px;
}

/* CORNER_RADIUS_V1 - the four corner radii belong to the TABLE's four corners. Two of them said
   `th`, with no `thead` qualifier. That is harmless on seven of the eight .data-tables because
   their only `th` are in the header row - but #monthlySalesTable has 21 `th` in its BODY (it is
   the only table with a th row-label column), so EVERY body row got a rounded top-left AND
   top-right corner. Seven stacked 12px scallops down each edge = the "brick" pattern the operator
   spotted. Meanwhile the two BOTTOM corner rules below target `td`, and this table's last row is
   all `th` - so the corners that should be round were square. Exactly inverted.
   Only the radius moves to thead. text-align / position:sticky / z-index / min-width MUST stay on
   `.data-table th:first-child`: that is what pins the label column, and scoping them to thead
   would unpin it.
   Same shape as the §103 divider rule and the §105 icon audit: a rule written for the header,
   landing on body cells that only this one table has. */
.data-table thead th:first-child {
    border-radius: 12px 0 0 0;
}

.data-table th:first-child {
    text-align: left;
    position: sticky;
    left: 0;
    z-index: 20;
    min-width: 100px;
}

.data-table thead th:last-child {
    border-radius: 0 12px 0 0;
}

.data-table td:first-child {
    position: sticky;
    left: 0;
    background-color: white;
    text-align: left;
    z-index: 1;
}

.data-table th.total,
/* TABLE_NAVY_MATCH_V1 - the pinned Total/Average column is the SAME navy as the header.
   FLATTEN_CHROME_V1 made it --navy-700, one step lighter, on the theory that it should read
   as a distinct band. It doesn't - the column meets the header row at the top-right corner,
   so the two navies touch and simply look like one of them is wrong. The column is already
   distinguished by being pinned and bold; it doesn't need a second signal.
   This rule is shared by Monthly Sales, Room Occupancy by Room Type, and every other
   .data-table - fixing it here fixes all of them. */
.data-table th.average,
.data-table td.total,
.data-table td.average {
    background: var(--navy);
    color: white;
    font-weight: bold;
    position: sticky;
    right: 0;
    z-index: 15;
}

.data-table .room-type {
    background-color: rgba(15, 31, 61, 0.1);
    font-weight: bold;
    border-bottom: 2px solid rgba(15, 31, 61, 0.2);
}

.data-table tbody tr {
    transition: background-color 0.2s ease;
}

/* HOVER_GOLD_V1 - gold, not blue. The rest of the dashboard already hovers gold
   (.top-bar-refresh:hover, .top-bar-lang:hover, .btn-secondary:hover in base.css all go
   --gold-500/600), and gold IS the brand's hover colour - it is literally the site's
   ocean_hover_primary_color. base.css even ships --gold-100 as the tint for exactly this.
   A blue row hover under a navy header just read as the table flickering.
   Every hover on this page now speaks the same language: metric cards, tabs, table rows,
   OTA items, timeline options. */
.data-table tbody tr:hover {
    background-color: var(--gold-100);
}

/* `td` -> `> *`, the same correction §103 made to the divider rules: #monthlySalesTable's last row
   is `th`, so these never fired and its bottom corners stayed square while its body rows wore the
   header's rounding. Now the radius lands where the table actually turns a corner. */
.data-table tbody tr:last-child > *:first-child {
    border-radius: 0 0 0 12px;
}

.data-table tbody tr:last-child > *:last-child {
    border-radius: 0 0 12px 0;
}

/* Charts */
.chart-container {
    width: 100%;
    height: 500px;
    margin: 30px 0;
    position: relative;
    padding: 20px;
    background: white;
    border-radius: 12px;
    box-shadow: 0 4px 15px rgba(15, 31, 61, 0.1);
    overflow: visible;
    box-sizing: border-box;
}

/* OTA Section */
.ota-section {
    display: flex;
    flex-direction: column;
    gap: 25px;
    width: 100%;
    position: relative;
    overflow: visible;
}

.ota-list {
    width: 100%;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 15px;
    padding: 20px;
    background: rgba(15, 31, 61, 0.05);
    border-radius: 12px;
    border: 1px solid rgba(15, 31, 61, 0.1);
}

.ota-item {
    display: flex;
    align-items: flex-start;
    padding: 15px;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s ease;
    background: white;
    border: 2px solid rgba(15, 31, 61, 0.1);
    margin: 0;
    min-width: 240px;
    max-width: 320px;
}

/* HOVER_GOLD_V1 */
.ota-item:hover {
    background: var(--gold-100);
    transform: translateY(-2px);
    box-shadow: 0 4px 15px rgba(160, 122, 34, 0.2);
}

.ota-item.inactive {
    opacity: 0.5;
    transform: none !important;
}

.ota-color {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    margin-right: 12px;
    margin-top: 4px;
    flex-shrink: 0;
    box-shadow: 0 2px 4px rgba(15, 31, 61, 0.2);
}

.ota-details {
    flex-grow: 1;
    min-width: 0;
}

.ota-details > div {
    line-height: 1.5;
    margin-bottom: 4px;
    white-space: normal;
}

.ota-details > div:first-child {
    font-weight: 600;
    font-size: 1rem;
    color: var(--navy);
}

.ota-details > div:nth-child(2),
.ota-details > div:nth-child(3),
.ota-details > div:nth-child(4) {
    font-size: 0.9rem;
    color: rgba(15, 31, 61, 0.8);
}

.ota-chart-container {
    width: 100%;
    height: 500px;
}

/* Property Timeline Debug Panel */
/* Responsive Design */
@media screen and (max-width: 1400px) {
    .metrics-row {
        grid-template-columns: repeat(2, 1fr);
    }
    
    .section {
      margin-bottom: 50px;
      padding: 30px;
      border-radius: 16px;
    }
}

@media screen and (max-width: 768px) {
    /* Logo section mobile */
    .logo {
        width: 50px;
        height: 50px;
    }
    
    .tabs {
        gap: 6px;
        padding: 0 15px;
    }
    
    .tab {
        padding: 10px 16px;
        font-size: 0.9rem;
    }
    
    .metrics-row {
        grid-template-columns: 1fr;
        gap: 20px;
    }
    
    .section {
        padding: 20px;
        margin-bottom: 30px;
    }
    
    .section h2, .section h3 {
        font-size: 1.5rem;
    }
    
    .metric-value {
        font-size: 1.8rem;
    }
    
    .filter-controls {
        gap: 10px;
    }
    
    .filter-controls select {
        min-width: 120px;
        font-size: 0.9rem;
    }
    
    .ota-list {
        grid-template-columns: 1fr;
        padding: 15px;
    }
    
    .ota-item {
        min-width: 200px;
        max-width: 100%;
    }
    
    .chart-container {
        height: 400px;
        padding: 15px;
    }
    
    .data-table {
        font-size: 0.8rem;
    }
}

.timeline-basis-container {
  text-align: center;
  margin: 25px 0;
  padding: 20px;
  background: var(--gray-50);
  border: 1px solid var(--hairline);
  border-radius: 12px;
  border: 2px solid rgba(15, 31, 61, 0.15);
}

.timeline-basis-title {
  font-weight: 600;
  font-size: 1.2rem;
  color: var(--navy);
  margin-bottom: 15px;
  font-family: var(--font-sans);
}

.timeline-options {
  display: flex;
  justify-content: center;
  gap: 40px;
  flex-wrap: wrap;
}

.timeline-option {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 20px;
  border-radius: 10px;
  cursor: pointer;
  transition: all 0.3s ease;
  background: rgba(255, 255, 255, 0.7);
}

/* HOVER_GOLD_V1 */
.timeline-option:hover {
  background: var(--gold-100);
  transform: translateY(-2px);
}

.timeline-option input[type="radio"] {
  width: 18px;
  height: 18px;
  accent-color: var(--navy);
}

/* Monthly Sales Table Group Separators — RETIRED by DIVIDER_STANDARD_V1.
   The comment said "Monthly Sales"; the selector said `.data-table`. Rows 3/5/7
   encode Monthly Sales' metric grouping (Volume | OTA gross | Beds24+payable |
   Net) — on its 7 rows that is the whole table. Every other .data-table
   inherited three literal row numbers that mean nothing in its own data:
   roomRevenueTable (120 rows) drew three heavy rules across the top 7 and
   abandoned the remaining 113; dailySummaryTable/taxDetailTable (32 flat days)
   the same.
   Monthly Sales keeps these rules via MS_RULES_V1 (#monthlySalesTable, `> *`,
   .18 alpha) further down, which already outranks this block (1,1,2 vs 0,2,3) —
   so deleting it changes NOTHING on Monthly Sales (verified: rows 3/5 render
   rgba(15,31,61,.18) today, i.e. MS_RULES_V1 is what you already see). The other
   seven tables lose lines that were never about them.
   Real group boundaries for the room tables are marked by DIVIDER_STANDARD_V1. */

/* Collapsible Chart Sections */
.collapsible-section {
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  margin-bottom: 20px;
  overflow: hidden;
  background: white;
}

/* FLATTEN_CHROME_V1 - solid navy. Unlike the metric cards this one IS clickable
   (toggleCollapse), so it keeps a hover - but a colour step, not a lift. The lift
   + heavy shadow made a section header look like a floating button. */
.collapsible-header {
  background: var(--navy);
  color: white;
  padding: 15px 20px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  user-select: none;
  transition: background var(--transition-base);
  border: none;
}

.collapsible-header:hover {
  background: var(--navy-700);
}

.collapsible-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease-in-out, padding 0.5s ease-in-out;
  padding: 0 20px;
}

.collapsible-content.expanded {
  padding: 20px;
}

.collapse-icon {
  font-size: 18px;
  transition: transform 0.3s ease;
}

.collapsible-header.collapsed .collapse-icon {
  transform: rotate(-90deg);
}

/* Ensure full table display when expanded */
.collapsible-content .table-container {
  width: 100%;
  overflow-x: auto;
}

.collapsible-content .data-table {
  width: 100%;
  table-layout: auto;
}

.collapsible-content .filter-controls {
  margin-bottom: 20px;
}

/* FLATTEN_CHROME_V1 - solid navy; a PDF button is a secondary action and does not
   need the brand's full gradient shouting from the corner of every section. */
.chart-pdf-btn {
    position: absolute;
    top: 20px;
    right: 20px;
    background: var(--navy);
    color: white;
    border: none;
    padding: 8px 16px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.85rem;
    z-index: 10;
}

.chart-pdf-btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(15, 31, 61, 0.3);
}

/* Performance and caching optimizations */
.tab.loading {
  position: relative;
  opacity: 0.7;
  transition: opacity 0.3s ease;
}

.tab.loading::after {
  content: '⋯';
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  animation: subtlePulse 1.5s infinite;
  color: var(--gold);
  font-weight: bold;
}

@keyframes subtlePulse {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 1; }
}

/* Instant display optimizations */
.metric-card {
  transition: all 0.3s ease;
}

/* FLATTEN_CHROME_V1 - transient refresh state: tint the border gold rather than
   repaint the surface, so the card never changes what its text sits on. */
.metric-card.updating {
  border-color: var(--gold);
}

/* Mobile optimizations for caching */
@media (max-width: 768px) {
  .chart-container {
    height: 250px !important;
    padding: 8px;
  }
  
  .metrics-row {
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
  }
  
  .data-table {
    font-size: 0.85rem;
  }

  /* MOBILE_SECTION_UNHIDE_V1 (2026-07-20) — REMOVED, do not reintroduce:
       .section[data-view]:not(.active) { display: none !important; }
     Nothing in the codebase ever sets .active on a section; the tab switcher
     assigns inline style.display, and a stylesheet !important beats an inline
     style. So at <=768px EVERY data-view section computed to none — proven live
     at 390px: monthlyOccupancySection had inline "block", computed "none". Every
     tab but Overview rendered headings and no content, and Chart.js — which
     reads parent size at construction — locked its canvases to 0x0 (calling
     .resize() on a live instance with a healthy 530x250 parent did NOT recover
     it). Tab visibility is already handled in JS; this rule was pure damage. */
}

/* ===================================================================
   MOBILE_TOPBAR_V1 (2026-07-20)
   The @media 768px block above still styles .header-container /
   .hotel-title / .hotel-subtitle — all three have ZERO occurrences in
   the current index.html. They were the Apps Script header, replaced by
   .an-topbar (GAS_DEADCODE_V1), which never received mobile rules. Result
   measured at 390px: .an-right ran from x=312 to x=548, so the whole page
   scrolled sideways (documentElement.scrollWidth 548 vs viewport 390).
   Placed AFTER the .an-topbar base rules on purpose — same specificity
   (0,2,0), so source order is what makes these win.
   =================================================================== */
@media (max-width: 768px) {
  .an-topbar {
    height: 54px;
    padding: 0 12px;
    gap: 8px;
  }
  /* let the brand cluster be the thing that yields, not the controls */
  .an-topbar .an-logo-wrap { min-width: 0; gap: 8px; }
  .an-topbar .an-logo-wrap img { height: 30px; }
  .an-topbar .an-brand {
    font-size: 1.05rem;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .an-topbar .an-right { gap: 8px; flex-shrink: 0; }
  .an-topbar .an-logout { padding: 7px 12px; font-size: 0.72rem; }
}

@media (max-width: 560px) {
  /* SP-000 is the least load-bearing thing in the bar — it is identity, not
     control, and it is still on every API call. Dropping it buys ~58px, which
     is what keeps Refresh / JP / Logout on screen at 390px. */
  .an-topbar .an-user { display: none; }
  .an-topbar .an-brand .an-accent { display: none; }  /* "S-Presso" alone reads fine */
  .an-topbar .an-logout { padding: 7px 10px; }
}

/* Reduce reflow during dynamic updates */
.chart-container {
  contain: layout style paint;
}

.data-table {
  contain: layout style;
}

/* Metric updating animation */
.metric-card.updating::after {
  content: '⟳';
  position: absolute;
  top: 8px;
  right: 8px;
  animation: spin 1s linear infinite;
  color: var(--gold);
  font-size: 0.9rem;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Enhanced metric updating */
.metric-card.updating {
  transform: scale(1.01);
  box-shadow: 0 4px 15px rgba(160, 122, 34, 0.2);
}

/* GAS_DEADCODE_V1 - .bg-teal removed: zero references in HTML or JS, and a teal
   (#14b8a6) that exists in neither base.css nor the website's palette. */

/* GAS_DEADCODE_V1 - .logo-header + .spresso-logo (and their two media queries) removed:
   more residue from the Apps Script header that .an-topbar replaced. Zero references in
   analytics/index.html or analytics.js. .logo-header still carried background:#f0f2f5 -
   the page background from before it went cream - so it was stale twice over. */

/* =============================================
   QUICK ACCESS - GOOGLE APP DRAWER STYLE
   ============================================= */

/* AN_FAB_BOTTOMRIGHT_V1 */
/* AN_FAB_MIDRIGHT_V1 */
.app-drawer-button {
  /* AN_FAB_EDGE_V1 */
  position: fixed;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  z-index: 99999;
  width: 40px;
  height: 40px;
  background: white;
  border: 1px solid #dadce0;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  color: #5f6368;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  transition: all 0.2s ease;
}

.app-drawer-button:hover {
  background: #f8f9fa;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

/* SPLASH_CHROME_HIDE_V2 (2026-07-20) — the Quick Access FAB is STATIC markup with z-index 99999
   against the splash's 9999, so it painted instantly and sat on top of the loading screen.
   V1 hid it with a JS-applied `html.an-splash` class and FLASHED. Cause: **Cloudflare Rocket
   Loader**. It rewrites every script tag's type (`type="<hash>-text/javascript"`) so the browser
   skips them during parse and Rocket Loader runs them later — AFTER first paint. Any "hide it with
   an inline script" approach is therefore guaranteed to flash on this site.
   V2 inverts it: **hidden is the CSS DEFAULT**, so it applies at parse time with no script
   involved, and JS only ever ADDS the reveal. A deferred script can delay the fade-in (harmless);
   it can no longer cause a flash of visible chrome.
   `html.an-ready` is set by hideSimpleLoading(), which every authenticated load reaches (directly,
   via the readiness poll, or via the 45s failsafe). Unauthenticated visitors never reveal the page
   at all, so the FAB staying hidden for them is correct.
   opacity+pointer-events rather than display:none so nothing reflows and it fades in with the page.

   SCOPE — the BUTTON only. V2 first covered .app-drawer-overlay and .app-drawer-panel too, which
   was a regression: both already declare their own `opacity:0; visibility:hidden` and are revealed
   by an `.open` class, but `html.an-ready .app-drawer-panel` (0,2,1) outranks `.app-drawer-panel`
   (0,1,0), so it pinned the panel to opacity:1 permanently — only its `visibility:hidden` was
   accidentally still hiding it. They hide themselves by default and never flashed; they need no
   rule here. */
.app-drawer-button {
  opacity: 0;
  pointer-events: none;
}
html.an-ready .app-drawer-button {
  opacity: 1;
  pointer-events: auto;
}
.app-drawer-button { transition: all 0.2s ease, opacity 0.35s ease; }

.app-drawer-panel {
  position: fixed;
  top: 50%;
  right: 52px;
  z-index: 99998;
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  min-width: 280px;
  max-width: 320px;
  max-height: 500px;
  overflow-y: auto;
  opacity: 0;
  visibility: hidden;
  transform: translate(0, calc(-50% + 10px));
  transition: all 0.2s ease;
}

.app-drawer-panel.active {
  opacity: 1;
  visibility: visible;
  transform: translate(0, -50%);
}

.app-drawer-header {
  padding: 16px 20px;
  border-bottom: 1px solid #e8eaed;
  font-weight: 600;
  color: var(--navy);
  font-size: 0.95rem;
}

.app-drawer-group-title {
  padding: 8px 20px 4px;
  font-size: 0.75rem;
  font-weight: 600;
  color: #5f6368;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.app-drawer-item {
  padding: 12px 20px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 12px;
  color: #202124;
  font-size: 0.9rem;
  transition: background 0.15s ease;
}

.app-drawer-item:hover {
  background: #f1f3f4;
}

.app-drawer-item-icon {
  width: 20px;
  text-align: center;
  font-size: 1rem;
}

.app-drawer-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 99997;
  background: transparent;
  display: none;
}

.app-drawer-overlay.active {
  display: block;
}

/* CINQS_BRAND_V1 - taupe/cream/gold chrome override, applied only when html[data-brand=cinqs] */
html[data-brand="cinqs"] .an-topbar { background: #5d4f4a; }
html[data-brand="cinqs"] .an-topbar .an-brand .an-accent { color: #ebe4d6; }
html[data-brand="cinqs"] .an-topbar .an-icon-btn:hover,
html[data-brand="cinqs"] .an-topbar .an-langbtn:hover { color: #5d4f4a; }
html[data-brand="cinqs"] .tab.active { background: #5d4f4a; border-color: #5d4f4a; color: #fff; }
html[data-brand="cinqs"] .tab.active * { color: #fff; }

/* CINQS_BRAND_DEPTH_V1 - full GAS-parity brand inheritance across all surfaces (scoped to data-brand=cinqs) */
html[data-brand="cinqs"] .section::before { background: #5d4f4a; }
/* BASECSS_ADOPT_V1 - CINQS honours the same card contract as S-Presso: one white
   surface, brand colour as a rule. Without this the taupe gradient returned and
   put the new navy ink on #5d4f4a at 2.09:1 - re-creating, for CINQS users only,
   the exact bug BRAND_TOKENS_V1 removed. Taupe #5d4f4a matches the site's
   --cinqs-taupe, so this stays on-brand. */
/* CARD_UNIFORM_V1 - CINQS follows the same contract: four identical white cards, no
   per-slot rule. (These overrides are now redundant with the base rule, but kept so a
   future CINQS surface change has an obvious place to live.) */
html[data-brand="cinqs"] .bg-blue,
html[data-brand="cinqs"] .bg-slate,
html[data-brand="cinqs"] .bg-navy,
html[data-brand="cinqs"] .bg-gold { background: var(--white); }
html[data-brand="cinqs"] .metric-card:hover { border-color: #5d4f4a; }
html[data-brand="cinqs"] .data-table th,
html[data-brand="cinqs"] .data-table th.average,
html[data-brand="cinqs"] .data-table td.total,
html[data-brand="cinqs"] .data-table td.average { background: #5d4f4a; }
html[data-brand="cinqs"] .collapsible-header { background: #5d4f4a; }
html[data-brand="cinqs"] .collapsible-header:hover { background: #6d5e59; }
html[data-brand="cinqs"] .chart-pdf-btn { background: #5d4f4a; }
html[data-brand="cinqs"] h2,
html[data-brand="cinqs"] h3 { color: #5d4f4a; }
html[data-brand="cinqs"] .filter-controls select { color: #5d4f4a; }
html[data-brand="cinqs"] .filter-controls select:focus { border-color: #8a7260; box-shadow: 0 0 0 3px rgba(138,114,96,0.18); }
html[data-brand="cinqs"] .ota-details > div:first-child { color: #5d4f4a; }
html[data-brand="cinqs"] .timeline-basis-title { color: #5d4f4a; }
html[data-brand="cinqs"] .timeline-option input[type="radio"] { accent-color: #5d4f4a; }
html[data-brand="cinqs"] .app-drawer-header { color: #5d4f4a; }
html[data-brand="cinqs"] .tab:hover { color: #5d4f4a; }

/* CINQS_BRAND_POLISH_V1 - logo size+spacing, active-tab hover, subtitle contrast (scoped) */
/* bigger logo, more breathing room from the title */
html[data-brand="cinqs"] .an-topbar .an-logo-wrap { gap: 18px; }
html[data-brand="cinqs"] .an-topbar .an-logo-wrap img { height: 54px; }
/* active tab on hover must keep taupe bg + white text (base .tab:hover whitens bg -> white-on-white) */
html[data-brand="cinqs"] .tab.active:hover { background: #5d4f4a; border-color: #5d4f4a; color: #fff; box-shadow: 0 2px 8px rgba(93,79,74,0.25); }
html[data-brand="cinqs"] .tab.active:hover * { color: #fff; }
/* non-active tab hover: readable taupe text on white */
html[data-brand="cinqs"] .tab:hover { border-color: #8a7260; color: #5d4f4a; }
/* lift faint subtitle text so it is readable on the brand cards */
html[data-brand="cinqs"] .metric-subtitle { opacity: 0.95; }

/* CINQS_BAR_HEIGHT_V1 - taller bar so the larger CINQS logo fits with padding (scoped) */
html[data-brand="cinqs"] .an-topbar { height: 72px; }
html[data-brand="cinqs"] .an-topbar .an-logo-wrap img { height: 50px; }

/* ============================================================================
   MS_DESIGN_V1 (staging) — Monthly Sales Data. CSS ONLY.
   No JS is touched: data, labels, the MoM/YoY badges and the column set are
   exactly as live. Scoped to #monthlySalesTable so every other .data-table
   (Room Occupancy etc.) is untouched.
   ============================================================================ */

/* 1. DEMOTE THE NAVY — the main event.
   `.data-table th` (line ~382) paints EVERY th navy: the header row, the whole
   row-label column, AND the pinned Total/Average. Three saturated slabs frame
   the data on three sides, so the eye lands on the chrome instead of the
   numbers. One navy header row is the anchor; the rest gets its light back. */
#monthlySalesTable tbody th {
    background: #fff;
    color: var(--navy);
    font-weight: 600;
    border-right: 1px solid rgba(15, 31, 61, .10);
}

/* Total/Average earn prominence from a tint + weight + a hairline, not a slab. */
#monthlySalesTable tbody th.total,
#monthlySalesTable tbody th.average {
    background: var(--gray-50);
    color: var(--navy);
    border-left: 1px solid rgba(15, 31, 61, .12);
}

/* 2. Total and Average are BOTH `sticky; right:0` — the same offset — so they
   land on top of each other and Total is never visible (check your original
   screenshot: there is no Total column). Only Average needs pinning. */
#monthlySalesTable th.total {
    position: static;
}

/* 3. Figures are compared DOWN a column, so they align on the digit, not the
   left edge. Right-align + tabular-nums is what makes ¥37,861,300 and
   ¥5,522,685 read as magnitudes instead of as strings. */
#monthlySalesTable thead th,
#monthlySalesTable tbody td,
#monthlySalesTable tbody th.total,
#monthlySalesTable tbody th.average {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
#monthlySalesTable thead th:first-child,
#monthlySalesTable tbody th:first-child {
    text-align: left;
}

/* 4. The MoM/YoY badge sits BESIDE the figure, which roughly doubles every
   column's width — that is what forces the horizontal scroll and pushes Average
   off the edge. Its colour and weight are inline (untouchable from here, and
   already the contrast-fixed TOK values from §88), but size and layout are not:
   drop it onto its own line under the number. Figure gets primacy, delta stays
   readable, column halves in width. */
#monthlySalesTable tbody td span {
    display: block;
    font-size: 0.72rem;
    line-height: 1.3;
    margin-top: 1px;
}

/* 5. Tighter rhythm — 12px/15px was set when each cell held one short number. */
#monthlySalesTable th,
#monthlySalesTable td {
    padding: 9px 12px;
}

/* 6. The rows are a derivation chain, not peers. Bands after the volume/rate block
   and after Payable Commission (the last input to Net).
   REVPAR_V1 (2026-07-20): 2/6 → 3/7. RevPAR joined the volume/rate block as row 3, so the
   band closing that block moved from ADR to RevPAR, and Payable Commission moved 6 → 7. This
   is the THIRD rule keyed to literal MS row numbers (see also the group-start and group-end
   rules ~1590/1602); it was invisible while the later rule overrode it, and only surfaced in
   the browser when that one was renumbered. Insert a row here and ALL THREE must move. */
#monthlySalesTable tbody tr:nth-child(3) > *,
#monthlySalesTable tbody tr:nth-child(7) > * {
    border-bottom: 2px solid rgba(15, 31, 61, .18);
}

/* 7. Net Revenue is the bottom line and should read like one. Its td carries an
   inline font-weight/colour already, so this only adds the rule above it. */
#monthlySalesTable tbody tr:last-child > * {
    border-top: 2px solid var(--navy);
}
#monthlySalesTable tbody tr:last-child th:first-child {
    font-weight: 700;
}

/* ============================================================================
   MS_HEADER_V1 (staging) — Monthly Sales Data section header. CSS ONLY.
   The Timeline Basis block spends ~145px on ONE either/or: a 2px-bordered card,
   a 1.2rem centred heading, two 12px/20px pill radios and a translateY hover
   lift. Collapsed to a segmented control it costs ~34px and sits on the same
   line as the three filters that scope the same table.
   Scoped to #monthlySalesSection — Commission/Daily Summary keep their current
   look until you say roll it out.
   ============================================================================ */

/* Filters and the timeline share one line: both inline-flex, so they flow
   together instead of stacking as two block rows. */
#monthlySalesSection .filter-controls {
    display: inline-flex;
    margin-bottom: 0;
    margin-right: 14px;
    vertical-align: middle;
}
#monthlySalesSection .timeline-basis-container {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    text-align: left;
    vertical-align: middle;
}
/* Vertical margins don't apply to inline-level boxes, so the gap before the
   table lives on the table instead. */
#monthlySalesSection .table-container {
    margin-top: 18px;
}

/* "Timeline Basis:" is a field label, not a section heading. */
#monthlySalesSection .timeline-basis-title {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--muted);
    margin: 0;
    white-space: nowrap;
}

/* Two mutually exclusive options = a segmented control. */
#monthlySalesSection .timeline-options {
    gap: 0;
    border: 1px solid rgba(15, 31, 61, .18);
    border-radius: 8px;
    overflow: hidden;
    flex-wrap: nowrap;
}
#monthlySalesSection .timeline-option {
    gap: 0;
    padding: 7px 13px;
    border-radius: 0;
    background: #fff;
    transition: background .15s ease, color .15s ease;
    font-size: 0.82rem;
    white-space: nowrap;
}
#monthlySalesSection .timeline-option + .timeline-option {
    border-left: 1px solid rgba(15, 31, 61, .18);
}
/* The radio is redundant once the selected pill is filled — the label IS the
   control. Kept in the DOM and focusable, just not painted. */
#monthlySalesSection .timeline-option input[type="radio"] {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
}
#monthlySalesSection .timeline-option:has(input:checked) {
    background: var(--navy);
    color: #fff;
}
#monthlySalesSection .timeline-option:has(input:checked) small {
    color: rgba(255, 255, 255, .72);
}
/* A radio button should not levitate. */
#monthlySalesSection .timeline-option:hover {
    background: var(--gold-100);
    transform: none;
}
#monthlySalesSection .timeline-option:has(input:checked):hover {
    background: var(--navy);
}
/* Keyboard users must still see focus once the radio itself is invisible. */
#monthlySalesSection .timeline-option:has(input:focus-visible) {
    outline: 2px solid var(--gold);
    outline-offset: -2px;
}

/* Title and PDF share the top line; h3's 25px bottom margin was sized for a
   heading with nothing under it. */
#monthlySalesSection h3 {
    margin-bottom: 16px;
}

/* ---- table refinements (still CSS only) ---- */
/* The badges sit on every cell; at 0.72rem they still competed with the figure.
   Colour/weight are inline and stay untouched — only scale recedes. */
#monthlySalesTable tbody td span {
    font-size: 0.68rem;
    opacity: .82;
}
/* Rows were ~60px once the badge took a second line. */
#monthlySalesTable th,
#monthlySalesTable td {
    padding: 7px 12px;
}
/* Average is the column people read; give it one step more presence than Total
   so the eye lands on it rather than sliding past.
   MUST be an OPAQUE colour: this column is position:sticky, so a translucent
   fill lets the scrolling month cells show straight through it. An rgba() here
   rendered the Average figures on top of 2026-09's — caught in the browser,
   invisible in the CSS. */
#monthlySalesTable tbody th.average {
    background: var(--gray-100);
}

/* ============================================================================
   MS_TOOLBAR_V1 (staging) — one toolbar row for Monthly Sales Data.
   Title, the three filters, the timeline control and PDF all scope/act on the
   same table, so they belong on one line. Previously: title row + filter row +
   a 145px timeline card = three bands before any data.
   ============================================================================ */

/* Title joins the row instead of owning one. 1.55rem was sized for a heading
   standing alone; beside 34px controls it just shouts. */
#monthlySalesSection h3 {
    display: inline-block;
    vertical-align: middle;
    margin: 0 18px 0 0;
    font-size: 1.3rem;
}

/* The three selects were a different design language from the segmented control
   they sit beside: 44px tall vs 34px, 10px radius vs 8px, a drop shadow and a
   translateY hover lift the segmented control doesn't have. Match them. */
#monthlySalesSection .filter-controls {
    gap: 8px;
}
#monthlySalesSection .filter-controls select {
    height: 34px;
    padding: 0 10px;
    border: 1px solid rgba(15, 31, 61, .18);
    border-radius: 8px;
    font-size: 0.82rem;
    font-weight: 500;
    min-width: 0;
    box-shadow: none;
    transition: border-color .15s ease;
}
/* Width follows the longest <option> ("Sakimoto Residence Namba Minami 2"),
   which is why a 15-character label got a 340px box. Cap each to its job. */
#monthlySalesSection #monthlySalesPropertySelector { width: 190px; }
#monthlySalesSection #monthlySalesYearSelector     { width: 88px; }
#monthlySalesSection #monthlySalesMonthSelector    { width: 124px; }

#monthlySalesSection .filter-controls select:hover {
    border-color: var(--gold-600);
    box-shadow: none;
    transform: none;
}
#monthlySalesSection .filter-controls select:focus {
    box-shadow: 0 0 0 3px rgba(160, 122, 34, .15);
}

/* PDF is an action on this table, so it belongs on the control row, not floating
   at the card's corner. Still absolute (it precedes h3 in the DOM), just aligned
   to the row rather than to the card. */
#monthlySalesSection .chart-pdf-btn {
    top: 30px;
    right: 30px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 34px;
    padding: 0 13px;
    border-radius: 8px;
    font-size: 0.82rem;
    font-weight: 600;
    transition: background .15s ease;
}
#monthlySalesSection .chart-pdf-btn svg {
    width: 15px;
    height: 15px;
    flex: none;
}
#monthlySalesSection .chart-pdf-btn:hover {
    background: var(--navy-700);
    transform: none;
    box-shadow: none;
}

/* The PDF button is position:absolute, so it reserves no width — the toolbar has
   to leave its own gap. 100px here was enough to push the timeline onto a second
   row despite ~490px of free space, so reserve just what the button occupies
   (~85px + 30px offset) and let the row wrap naturally below that. */
#monthlySalesSection .timeline-basis-container {
    margin-right: 0;
}
#monthlySalesSection .filter-controls {
    margin-right: 14px;
}
/* Reserve the button's lane so the toolbar can never run under it. */
#monthlySalesSection h3 {
    max-width: calc(100% - 120px);
}

/* ============================================================================
   MS_TOOLBAR_V2 (staging) — supersedes MS_TOOLBAR_V1's single row.
   V1 put the title on the control row. It saved ~40px of height and paid for it
   by squeezing every control: the property select had to be capped at 190px,
   which is narrower than its own longest option ("Sakimoto Residence Namba
   Minami 2"). Shrinking a control below the width of its data to win vertical
   space is a bad trade. Title goes back on top; the control bar gets the full
   width and spans the same measure as the table under it.
   ============================================================================ */

#monthlySalesSection h3 {
    display: block;
    margin: 0 0 14px 0;
    font-size: 1.4rem;
    max-width: none;
}

/* Each select sizes to its content again, no caps. */
#monthlySalesSection #monthlySalesPropertySelector,
#monthlySalesSection #monthlySalesYearSelector,
#monthlySalesSection #monthlySalesMonthSelector {
    width: auto;
    min-width: 0;
}

/* The control bar spans the table's measure: filters left, timeline after them,
   PDF hard right on the SAME line — so its right edge lines up with the table's
   right edge rather than with the card's corner. */
#monthlySalesSection .filter-controls {
    margin-right: 16px;
}
/* The button is position:absolute and sits BEFORE h3 in the DOM, so it can't be
   flowed into the control row — its offset has to be computed.
   30px section padding + 35.8px h3 line (1.4rem x body's 1.6 line-height) + 14px
   h3 margin = 79.8px. Both button and selects are 34px tall, so their tops meet. */
#monthlySalesSection .chart-pdf-btn {
    top: 80px;
    right: 30px;
}

/* ============================================================================
   MS_CONTROL_SIZE_V1 (staging) — controls read at the same size as the content.
   MS_TOOLBAR_V1 set every control to 0.82rem (~13px) so the selects would match
   the segmented control. But .data-table is 0.9rem (~14.4px), which made the
   things you CLICK smaller than the things you only READ — backwards. The right
   move was to raise the segmented control to the content, not lower the selects
   below it. Everything on the bar now sits at 0.9rem / 36px.
   ============================================================================ */

#monthlySalesSection .filter-controls select {
    height: 36px;
    font-size: 0.9rem;
    padding: 0 11px;
}

#monthlySalesSection .timeline-option {
    font-size: 0.9rem;
    padding: 7px 14px;
}

/* The "Timeline Basis:" caption is a label, not a control — it stays one step
   down so the control it names keeps the emphasis. */
#monthlySalesSection .timeline-basis-title {
    font-size: 0.84rem;
}

#monthlySalesSection .chart-pdf-btn {
    height: 36px;
    font-size: 0.88rem;
    padding: 0 14px;
}
#monthlySalesSection .chart-pdf-btn svg {
    width: 16px;
    height: 16px;
}

/* ============================================================================
   MS_RULES_V1 (staging) — dividers, hover, and type scale corrections.
   ============================================================================ */

/* --- TYPE: rejoin the page's scale ---------------------------------------
   MS_TOOLBAR_V1 shrank this h3 to fit the control row; V2 put the title back on
   its own line but never restored the size, leaving it 22.4px against every
   other section's 24.8px. And the selects were pulled to 14.4px when the rest of
   the page's selects are 15.2px. Both back to the global scale — which is still
   >= the table's 14.4px content, so the "control smaller than data" problem
   stays fixed. */
#monthlySalesSection h3 {
    font-size: 1.55rem;
}
#monthlySalesSection .filter-controls select,
#monthlySalesSection .timeline-option {
    font-size: 0.95rem;
}
#monthlySalesSection .chart-pdf-btn {
    font-size: 0.9rem;
}

/* --- ROW DIVIDERS --------------------------------------------------------
   The group rules already existed at nth-child(3)/(5)/(7) — but scoped to `td`,
   so each one stopped at the row-label column and never reached Total/Average.
   Half-width dividers on a full-width table. Extend them across every cell.
   REVPAR_V1 (2026-07-20): RevPAR was inserted as row 3, joining Bookings + ADR in the
   performance block, so every group START shifted down one: 3/5/7 → 4/6/8. These literal row
   numbers are the table's grouping (Volume+rate | OTA gross | Beds24+payable | Net) hard-coded
   as positions — ANY future row insertion must renumber both this rule and the group-END rule
   below, or the dividers silently land mid-group. */
#monthlySalesTable tbody tr:nth-child(4) > *,
#monthlySalesTable tbody tr:nth-child(6) > *,
#monthlySalesTable tbody tr:nth-child(8) > * {
    border-top: 2px solid rgba(15, 31, 61, .18);
}

/* MS_DESIGN_V1 added bands after rows 2 and 6 — the SAME boundaries the rules
   above already mark. With border-collapse:separate both drew: 4px under the
   data cells, 2px under the label. Stand down; the existing grouping was right
   (Volume | OTA gross | Beds24+payable | Net) and I simply hadn't read it.
   REVPAR_V1: group ENDS shifted 2/6 → 3/7 for the same reason (RevPAR now closes the
   performance block that ADR used to). */
#monthlySalesTable tbody tr:nth-child(3) > *,
#monthlySalesTable tbody tr:nth-child(7) > * {
    border-bottom: 1px solid rgba(15, 31, 61, .1);
}

/* Net Revenue keeps the navy rule — last, so it wins over the .18 above. */
#monthlySalesTable tbody tr:last-child > * {
    border-top: 2px solid var(--navy);
}

/* --- ROW HOVER -----------------------------------------------------------
   .data-table tbody tr:hover already tints the row gold-100, but a tr's
   background paints UNDER its cells — and my label/Total/Average cells carry
   their own fills, so the highlight died at both ends of the row. On a
   14-column table the hover is the main aid for tracking one metric across the
   year, so it has to span the whole row.
   The literal colour is deliberate: --gold-100 is rgba(160,122,34,.12), and
   these columns are position:sticky — a translucent fill would let the
   scrolling months show straight through (the bug caught earlier). This is that
   same tint pre-composited over white. */
#monthlySalesTable tbody tr:hover > th {
    background: #f4efe4;
}

/* ============================================================================
   MS_TOOLBAR_V3 (staging) — responsive toolbar. Supersedes the V1/V2 geometry.
   THE BUG: .chart-pdf-btn is position:absolute, so it reserves ZERO width. The
   toolbar had no idea it existed and flowed straight under it — at mid widths
   the PDF button sat on top of "Check-out Based". V2's `max-width: calc(100% -
   120px)` only fenced the h3, which is not what was colliding. Absolute
   positioning cannot participate in wrapping; the button has to rejoin the flow.
   The section has exactly 5 children (button, h3, filters, timeline, table), so
   flex + order does this with no markup change. .section::before (the gold rule)
   is absolute, so it never becomes a flex item.
   ============================================================================ */

#monthlySalesSection {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 12px;
}

/* Title claims a full line of its own. */
#monthlySalesSection h3 {
    order: 1;
    flex: 1 0 100%;
    margin: 0;
    max-width: none;
}

#monthlySalesSection .filter-controls {
    order: 2;
    display: flex;
    margin: 0;
}

#monthlySalesSection .timeline-basis-container {
    order: 3;
    display: flex;
    margin: 0;
}

/* Back in the flow: it now occupies real width, so the row wraps AROUND it
   instead of under it. margin-left:auto keeps it hard right on whatever line it
   lands on — so it right-aligns whether the toolbar is one line or two. */
#monthlySalesSection .chart-pdf-btn {
    order: 4;
    position: static;
    margin-left: auto;
    top: auto;
    right: auto;
}

#monthlySalesSection .table-container {
    order: 5;
    flex: 1 0 100%;
    margin-top: 8px;
}

/* ============================================================================
   MS_TOOLBAR_V4 (staging) — supersedes V3's attempt to flex the section itself.
   V3 failed silently: the view-switcher JS writes style="display:block" onto
   #monthlySalesSection, and inline beats the stylesheet, so `display:flex` never
   applied — which also made `order` and `margin-left:auto` inert. (!important is
   not available: the same JS writes display:none to hide the section on tab
   switch.) The controls now live in .ms-toolbar, a wrapper JS never touches.
   ============================================================================ */

/* Undo V3's section-level flex — it never worked, and shouldn't linger. */
#monthlySalesSection {
    display: block;
    gap: normal;
}
#monthlySalesSection h3 {
    order: initial;
    flex: initial;
    margin: 0 0 14px 0;
}
#monthlySalesSection .table-container {
    order: initial;
    flex: initial;
    margin-top: 18px;
}

/* The real toolbar. One wrapping flex row; every child reserves real width, so
   nothing can ever be flowed under anything else. */
#monthlySalesSection .ms-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 12px;
}
#monthlySalesSection .ms-toolbar .filter-controls,
#monthlySalesSection .ms-toolbar .timeline-basis-container {
    display: flex;
    order: initial;
    margin: 0;
}

/* In the flow at last. margin-left:auto keeps it hard right on whichever line it
   lands on, so it right-aligns whether the toolbar is one row or three. */
#monthlySalesSection .ms-toolbar .chart-pdf-btn {
    position: static;
    order: initial;
    margin-left: auto;
    top: auto;
    right: auto;
    flex: none;
}

/* ============================================================================
   MS_SELECT_THEME_V1 + MS_PDF_HOVER_V1 (staging)
   ============================================================================ */

/* --- PDF hover: rejoin the page's hover language -------------------------
   MS_TOOLBAR_V1 hovered to --navy-700. That isn't a hover state, it's just a
   different blue — it reads as "did something change?" rather than "this is
   interactive". Everything else on this page accents gold on hover:
   .tab:hover and .filter-controls select:hover both take --gold-600 borders
   (HOVER_GOLD_V1). A solid navy button can't take a gold border without
   shifting layout, so it takes a gold inset ring instead — same language,
   no reflow.
   --gold-500, not --gold-600: per GOLD_SCALE_V1 the two are NOT
   interchangeable — on navy, gold-500 is 6.76:1 and gold-600 only 4.13:1.
   500 is the gold for dark grounds. */
#monthlySalesSection .ms-toolbar .chart-pdf-btn:hover {
    background: var(--navy);
    box-shadow: inset 0 0 0 1px var(--gold-500);
    transform: none;
}

/* --- Select picker: the system blue, themed -----------------------------
   That bright blue is the OS highlight inside a native popup — for years it was
   simply not stylable, and the only way out was replacing <select> with a JS
   listbox (which would break the dynamic option population here). Chrome's
   Customizable Select changes that: `appearance: base-select` hands the picker
   to CSS.
   Wrapped in @supports on purpose: where it isn't supported the browser keeps
   its native picker and everything still works. Progressive enhancement, not a
   dependency — and the <option> elements are untouched, so
   populateAllPropertySelectors and the data-months generator are unaffected. */
@supports (appearance: base-select) {
    #monthlySalesSection .filter-controls select {
        appearance: base-select;
    }
    #monthlySalesSection .filter-controls select::picker(select) {
        appearance: base-select;
        background: #fff;
        border: 1px solid rgba(15, 31, 61, .18);
        border-radius: 8px;
        box-shadow: 0 10px 30px rgba(15, 31, 61, .16);
        padding: 4px;
        margin-top: 4px;
        max-height: 60vh;
        overflow-y: auto;
    }
    #monthlySalesSection .filter-controls option {
        padding: 6px 10px;
        border-radius: 5px;
        font-size: 0.9rem;
        color: var(--navy);
        background: transparent;
    }
    /* The theme's hover, in place of the system blue. */
    #monthlySalesSection .filter-controls option:hover,
    #monthlySalesSection .filter-controls option:focus {
        background: var(--gold-100);
        color: var(--navy);
        outline: none;
    }
    #monthlySalesSection .filter-controls option:checked {
        background: var(--navy);
        color: #fff;
    }
    /* base-select adds a ::checkmark column; the filled row already says which
       one is current, so the tick is redundant width. */
    #monthlySalesSection .filter-controls option::checkmark {
        display: none;
    }
    #monthlySalesSection .filter-controls select::picker-icon {
        color: var(--gray-500);
        transition: transform .15s ease;
    }
    #monthlySalesSection .filter-controls select:open::picker-icon {
        transform: rotate(180deg);
    }
}

/* ============================================================================
   MS_SELECT_WIDTH_V1 (staging) — stop the toolbar reflowing on selection.
   appearance:base-select makes a <select> size to its CURRENT value, not to its
   widest option — so picking "Hotel S-Presso Ebisu" after "S-Presso Hotels" grew
   the control and re-wrapped the whole toolbar. A control that changes size when
   you use it is a control that makes the page jump under the cursor.
   Pinned to the widest option instead. Measured, not guessed: at 500 15.2px Noto
   Sans, "└ Sakimoto Residence Namba Minami 2" is 290px of text + ~46px of padding
   and picker icon = 336px. Rounded to 340.
   Not capped smaller on purpose: truncating that name to "…Namba Minami…" makes
   it indistinguishable from "Sakimoto Residence Namba Minami" — the property you
   picked has to stay readable in the closed control.
   ============================================================================ */
@supports (appearance: base-select) {
    #monthlySalesSection #monthlySalesPropertySelector { width: 340px; }
    #monthlySalesSection #monthlySalesYearSelector     { width: 104px; }
    #monthlySalesSection #monthlySalesMonthSelector    { width: 148px; }

    /* Belt and braces: if a longer property name is ever added, ellipsis rather
       than blow the toolbar apart. */
    #monthlySalesSection .filter-controls select {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

/* ============================================================================
   MS_SELECT_ALIGN_V1 (staging) — restore the vertical centring base-select drops.
   A native <select> centres its value through internal UA machinery no CSS can
   see. `appearance: base-select` replaces that with an ordinary flex box — and
   the UA sheet leaves `align-items: normal` (i.e. stretch), so the value sat at
   the top of its 24.32px line box inside a 36px control instead of centred.
   Measured, not guessed: display=flex, align-items=normal, line-height=24.32px,
   height=36px, padding-block=0.
   Opting into base-select means opting into laying the control out yourself.
   ============================================================================ */
@supports (appearance: base-select) {
    #monthlySalesSection .filter-controls select {
        align-items: center;
        line-height: 1;
    }
    /* The picker icon holds the right edge; the value takes the rest. */
    #monthlySalesSection .filter-controls select::picker-icon {
        margin-left: auto;
        flex: none;
    }
}

/* ============================================================================
   MS_PDF_HOVER_V2 (staging) — supersedes V1's gold ring.
   V1 kept the navy fill and added a gold inset ring. That's a hover language
   nothing else here speaks: the table this button exports hovers its rows to
   --gold-100, so the button should land on the same tint. It inverts (navy fill
   -> gold tint, white ink -> navy ink), which is also what .an-topbar
   .an-icon-btn:hover already does.
   The tint, not a solid gold: measured, every solid gold fails AA for a 14.4px
   label -- white on --gold-600 is 3.96:1, navy on --gold-600 is 4.13:1, white on
   --gold-500 is 2.42:1. Navy on --gold-100 (composites to #f4efe4 over the white
   card, the identical value the rows hover to) is 14.27:1.
   The SVG is stroked with currentColor, so it follows the ink for free.
   ============================================================================ */
#monthlySalesSection .ms-toolbar .chart-pdf-btn:hover {
    background: var(--gold-100);
    color: var(--navy);
    box-shadow: inset 0 0 0 1px var(--gold-border);
    transform: none;
}

/* ============================================================================
   MS_DELTA_LEGIBILITY_V1 (staging) — undo my own contrast regression.
   MS_HEADER_V1 put `opacity: .82` on these spans to make them "recede". Opacity
   is not recession, it is contrast reduction, and it silently gave back most of
   what §88 fought for:
       --down-fg on white : 10.02:1  ->  6.44:1   (colour became rgb(150,70,70))
       --up-fg   on white :  9.11:1  ->  5.62:1
   §88 raised these deltas from 1.74-2.78:1 to 9.11/10.02:1. Draining a third of
   that for a visual nicety is exactly the trade that work existed to prevent.
   Full strength restored, and sized to 12px against the metric pills' 12.8px.
   NOT pills: the metric cards carry 4 of them; this table would carry ~90, which
   is the "a value on every data point" anti-pattern with a background attached.
   At full strength the table delta is 10.02:1 -- already better than the pill's
   own 7.87:1 -- so size and weight are all it needed.
   ============================================================================ */
#monthlySalesTable tbody td span {
    opacity: 1;
    font-size: 0.75rem;
    line-height: 1.35;
}

/* ============================================================================
   TOOLBAR_ROLLOUT_V1 (staging) — the Monthly Sales control bar, everywhere.
   Keyed on .ms-toolbar (built by the normaliser in index.html) and on the
   *PropertySelector / *YearSelector / *MonthSelector id convention, which all 42
   selects on the page follow — so no per-section rules are needed.
   Scope note: this is the FILTER BAR + PDF only. The Monthly Sales *table*
   redesign stays scoped to #monthlySalesTable; no other .data-table moves.
   ============================================================================ */

.ms-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 12px;
    margin-bottom: 18px;
}
.ms-toolbar .filter-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 14px 0 0;
}
/* Title sits on its own line above the bar (operator's call — the bar needs the
   full width, and a control must never be narrower than its own data). Only
   pattern-A titles: collapsible sections keep their h3 inside the navy header. */
.section > h3 {
    margin-bottom: 14px;
}

/* --- controls: one height, one radius, one border, one type size ---------
   Previously 44px tall / 10px radius / drop shadow / translateY hover lift,
   sitting beside a 34px flat segmented control. Two design languages per bar. */
.filter-controls select {
    height: 36px;
    padding: 0 11px;
    border: 1px solid rgba(15, 31, 61, .18);
    border-radius: 8px;
    font-size: 0.95rem;
    font-weight: 500;
    box-shadow: none;
    transition: border-color .15s ease;
}
.filter-controls select:hover {
    border-color: var(--gold-600);
    box-shadow: none;
    transform: none;
}
.filter-controls select:focus {
    box-shadow: 0 0 0 3px rgba(160, 122, 34, .15);
}

/* Width by role, not by section. Pinned rather than content-sized: base-select
   sizes a control to its CURRENT value, so an unpinned one resizes as you use it
   and the bar reflows under the cursor. 340px is the measured width of the
   longest option ("└ Sakimoto Residence Namba Minami 2" = 290px text + 46px of
   padding and picker icon) — capping tighter would truncate it into its sibling. */
.filter-controls select[id$="PropertySelector"] { width: 340px; }
.filter-controls select[id$="YearSelector"]     { width: 104px; }
.filter-controls select[id$="MonthSelector"]    { width: 148px; }

/* --- timeline card -> segmented control ---------------------------------
   Was ~145px per section for one either/or: bordered card, 1.2rem centred
   heading, padded pill radios, and a translateY lift on a radio button. */
.ms-toolbar .timeline-basis-container {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    text-align: left;
}
.ms-toolbar .timeline-basis-title {
    font-size: 0.84rem;
    font-weight: 600;
    color: var(--muted);
    margin: 0;
    white-space: nowrap;
}
.ms-toolbar .timeline-options {
    gap: 0;
    border: 1px solid rgba(15, 31, 61, .18);
    border-radius: 8px;
    overflow: hidden;
    flex-wrap: nowrap;
}
.ms-toolbar .timeline-option {
    gap: 0;
    padding: 7px 14px;
    border-radius: 0;
    background: #fff;
    font-size: 0.95rem;
    white-space: nowrap;
    transition: background .15s ease, color .15s ease;
}
.ms-toolbar .timeline-option + .timeline-option {
    border-left: 1px solid rgba(15, 31, 61, .18);
}
/* The radio is redundant once the chosen pill is filled — the label IS the
   control. Kept in the DOM and focusable, just not painted. */
.ms-toolbar .timeline-option input[type="radio"] {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
}
.ms-toolbar .timeline-option:has(input:checked)       { background: var(--navy); color: #fff; }
.ms-toolbar .timeline-option:has(input:checked) small { color: rgba(255, 255, 255, .72); }
.ms-toolbar .timeline-option:hover                    { background: var(--gold-100); transform: none; }
.ms-toolbar .timeline-option:has(input:checked):hover { background: var(--navy); }
.ms-toolbar .timeline-option:has(input:focus-visible) { outline: 2px solid var(--gold); outline-offset: -2px; }

/* --- PDF button ---------------------------------------------------------- */
.ms-toolbar .chart-pdf-btn {
    position: static;
    margin-left: auto;
    top: auto;
    right: auto;
    flex: none;
}
.chart-pdf-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 36px;
    padding: 0 14px;
    border-radius: 8px;
    font-size: 0.88rem;
    font-weight: 600;
    transition: background .15s ease, color .15s ease;
}
.chart-pdf-btn svg { width: 16px; height: 16px; flex: none; }

/* Hover to the same tint the tables hover their rows to. The literal #f4efe4 —
   not var(--gold-100) — is deliberate and load-bearing: gold-100 is
   rgba(160,122,34,.12), and these buttons sit on TWO different grounds (white
   cards and the navy collapsible headers). A translucent tint would render as
   two different colours; the pre-composited opaque value is identical on both.
   Navy ink on it is 14.27:1. A solid gold was never available — measured, white
   on --gold-600 is 3.96:1, navy on --gold-600 4.13:1, white on --gold-500
   2.42:1, all under the 4.5:1 floor for this 14px label. */
.chart-pdf-btn:hover {
    background: #f4efe4;
    color: var(--navy);
    box-shadow: inset 0 0 0 1px var(--gold-border);
    transform: none;
}

/* On the navy headers the button was navy-on-navy — an invisible box with white
   text floating in it, no affordance at all. Give it the same treatment the
   topbar icon buttons already use on that ground. */
.collapsible-header .chart-pdf-btn {
    background: rgba(255, 255, 255, .12);
    border: 1px solid rgba(255, 255, 255, .4);
}
.collapsible-header .chart-pdf-btn:hover {
    background: #f4efe4;
    border-color: #fff;
    color: var(--navy);
    box-shadow: none;
}

/* ============================================================================
   SELECT_THEME_ROLLOUT_V1 (staging) — themed picker on every filter select.
   That bright blue in an open dropdown is the OS highlight inside a native
   popup; for years the only escape was replacing <select> with a JS listbox,
   which here would mean reimplementing 42 dynamically-populated controls.
   `appearance: base-select` hands the picker to CSS instead.
   @supports-gated on purpose: unsupported browsers keep their native picker and
   everything still works. Chrome-only today — Safari/Firefox keep the blue.
   The <option> elements are untouched, so populateAllPropertySelectors and the
   data-months generators are unaffected.
   ============================================================================ */
@supports (appearance: base-select) {
    .filter-controls select {
        appearance: base-select;
        /* base-select replaces the native control's internal centring machinery
           with a plain flex box, and the UA sheet leaves align-items:normal
           (= stretch) — so the value sits at the TOP of its line box instead of
           centred. Opting into base-select means laying the control out yourself. */
        align-items: center;
        line-height: 1;
    }
    .filter-controls select::picker(select) {
        appearance: base-select;
        background: #fff;
        border: 1px solid rgba(15, 31, 61, .18);
        border-radius: 8px;
        box-shadow: 0 10px 30px rgba(15, 31, 61, .16);
        padding: 4px;
        margin-top: 4px;
        max-height: 60vh;
        overflow-y: auto;
    }
    .filter-controls option {
        padding: 6px 10px;
        border-radius: 5px;
        font-size: 0.9rem;
        color: var(--navy);
        background: transparent;
    }
    /* the theme's hover, in place of the system blue */
    .filter-controls option:hover,
    .filter-controls option:focus {
        background: var(--gold-100);
        color: var(--navy);
        outline: none;
    }
    .filter-controls option:checked {
        background: var(--navy);
        color: #fff;
    }
    /* base-select adds a ::checkmark column; the filled row already says which
       one is current, so the tick is redundant width. */
    .filter-controls option::checkmark {
        display: none;
    }
    .filter-controls select::picker-icon {
        color: var(--gray-500);
        margin-left: auto;
        flex: none;
        transition: transform .15s ease;
    }
    .filter-controls select:open::picker-icon {
        transform: rotate(180deg);
    }
}

/* ============================================================================
   COLLAPSE_HEADER_HOVER_V1 (staging) — the expandable headers hover to the same
   tint as the PDF button and the table rows, not to a lighter blue.
   `.collapsible-header:hover { background: var(--navy-700) }` (#2c4a7c) was the
   same error as MS_PDF_HOVER_V1: a different blue reads as "did something
   change?", not as "this is interactive". Everything else here accents gold.
   #f4efe4 is --gold-100 pre-composited — opaque, so it is the identical value on
   the navy header that the rows get on white. Navy ink on it is 14.27:1.
   ============================================================================ */
.collapsible-header:hover {
    background: #f4efe4;
    color: var(--navy);
}

/* The h3 carries an inline style="color: white", and inline beats the
   stylesheet — without !important the title stays white and disappears on the
   tint. Safe here: it is a static attribute in the markup, nothing writes it at
   runtime, so we are not fighting live JS. */
.collapsible-header:hover h3 {
    color: var(--navy) !important;
}
.collapsible-header:hover .collapse-icon {
    color: var(--navy);
}

/* The PDF button sits INSIDE the header, so a header hover would leave its
   translucent-white fill and white border invisible on the tint. Flip it to
   navy so it stays a button. */
.collapsible-header:hover .chart-pdf-btn {
    background: var(--navy);
    border-color: transparent;
    color: #fff;
}
/* Cursor actually ON the button: it must still read as the deeper target, so it
   takes the gold ring rather than matching its own container. */
.collapsible-header .chart-pdf-btn:hover,
.collapsible-header:hover .chart-pdf-btn:hover {
    background: var(--navy);
    color: #fff;
    border-color: var(--gold-500);
    box-shadow: inset 0 0 0 1px var(--gold-500);
}

/* ============================================================================
   COLLAPSE_HEADER_HOVER_V2 (staging) — supersedes V1's navy+ring compromise.
   V1 made the in-header PDF hover to navy + a gold ring, unlike every other PDF
   button on the page. That was a workaround, not a fix: the button lives INSIDE
   the header, so hovering it also hovers the header, the bar went #f4efe4, and a
   #f4efe4 button on a #f4efe4 bar is invisible. I changed the button instead of
   the thing actually causing it.
   Real fix: while the cursor is on the button, the header does not take its own
   hover. Then the button can use the identical hover as everywhere else.
   This is also semantically right, not just cosmetic. The button's handler is
   `event.stopPropagation(); downloadChartPDF(...)` — clicking it does NOT
   collapse the section. Showing the header's "click to collapse" hover while the
   cursor is on the button advertises an action that will not happen.
   :has() carries its argument's specificity, so these beat V1's rules; the h3
   still needs !important to out-rank its inline style="color:white".
   ============================================================================ */
.collapsible-header:has(.chart-pdf-btn:hover) {
    background: var(--navy);
    color: #fff;
}
.collapsible-header:has(.chart-pdf-btn:hover) h3 {
    color: #fff !important;
}
.collapsible-header:has(.chart-pdf-btn:hover) .collapse-icon {
    color: #fff;
}

/* Now identical to the Monthly Sales button — same tint, same ink, same ring. */
.collapsible-header .chart-pdf-btn:hover,
.collapsible-header:hover .chart-pdf-btn:hover {
    background: #f4efe4;
    color: var(--navy);
    border-color: var(--gold-border);
    box-shadow: inset 0 0 0 1px var(--gold-border);
}

/* ============================================================================
   GUEST_PROFILE_MERGE_V1 (staging) — the merged Guest & Booking Profile card.
   Purely presentational: the panels hold the four original sections' own
   markup, filter bars and canvases verbatim.
   ============================================================================ */

#guestProfileSection .gp-switch {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 4px;
    flex-wrap: wrap;
}
#guestProfileSection .gp-switch-label {
    font-size: 0.84rem;
    font-weight: 600;
    color: var(--muted);
    white-space: nowrap;
}
/* Same segmented control as the timeline basis — one switch pattern on the page. */
#guestProfileSection .gp-seg {
    display: inline-flex;
    border: 1px solid rgba(15, 31, 61, .18);
    border-radius: 8px;
    overflow: hidden;
}
#guestProfileSection .gp-seg button {
    font: inherit;
    font-size: 0.95rem;
    font-weight: 600;
    padding: 7px 14px;
    border: 0;
    cursor: pointer;
    background: #fff;
    color: var(--navy);
    white-space: nowrap;
    transition: background .15s ease, color .15s ease;
}
#guestProfileSection .gp-seg button + button {
    border-left: 1px solid rgba(15, 31, 61, .18);
}
#guestProfileSection .gp-seg button[aria-pressed="true"] {
    background: var(--navy);
    color: #fff;
}
#guestProfileSection .gp-seg button:not([aria-pressed="true"]):hover {
    background: var(--gold-100);
}
#guestProfileSection .gp-seg button:focus-visible {
    outline: 2px solid var(--gold);
    outline-offset: -2px;
}

/* Each panel still carries its original section's own h3 — that title holds real
   information the switch label can't ("(Top 15)" is a caveat, not decoration),
   so it stays, demoted to a caption under the card title rather than deleted. */
#guestProfileSection .gp-panel > h3 {
    font-size: 1rem;
    font-weight: 600;
    color: var(--muted);
    margin: 14px 0 12px;
}
#guestProfileSection .gp-panel[hidden] {
    display: none;
}

/* GUEST_PROFILE_DISPLAY_V1 — the metric switch sits beside the dimension switch,
   one step smaller: it is subordinate to it (you pick a dimension, then a metric). */
#guestProfileSection .gp-metric {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-left: 6px;
}
#guestProfileSection .gp-metric[hidden] { display: none; }
#guestProfileSection .gp-seg-sm button {
    font-size: 0.85rem;
    padding: 5px 11px;
}

/* ============================================================================
   DIVIDER_STANDARD_V1 (staging) — make Monthly Sales' separator vocabulary the
   page standard, and apply it to the tables that never got it.

   THE PATTERN THIS FIXES. §99-§101 fixed Monthly Sales by layering ID-scoped
   overrides on top of shared `.data-table` rules. That fixed the page we were
   looking at and left every other table on the original, buggy rule. Measured in
   the browser today, all three of Monthly Sales' known bugs were still live on
   the room tables:
     - the nth-child(3)/(5)/(7) group rules (retired above — wrong table's
       semantics, inherited by seven tables);
     - the navy Total/Average SLAB that MS_DESIGN_V1 demoted (still navy here);
     - BUG C: th.total and th.average are BOTH `sticky; right:0`, so Total lands
       under Average and is never visible. §98 inferred it, §100 confirmed it in
       the browser for Monthly Sales. #roomRevenueTable has it TOO: .total renders
       x=1319..1440 and .average x=1328..1440 — overlapping, today.

   Scope is deliberately narrow: only #roomRevenueTable and #roomOccupancyTable
   carry .total/.average (verified across all 8 .data-tables — the other six have
   neither, so nothing else moves).
   ============================================================================ */

/* --- 1. Monthly Sales: the bottom line stopped two cells short --------------
   MS_RULES_V1 writes `#monthlySalesTable tbody tr:last-child > *` — `> *`, so it
   already targets th.total/th.average. It still lost: tables.js:560 emits
   `<th class="total" style="...border: 1px solid #e2e8f0;...">` and INLINE BEATS
   THE STYLESHEET (the documented trap, hit again). Measured: cells 0-12 render
   2px rgb(15,31,61); Total and Average render 1px rgb(226,232,240). The heaviest
   rule in the table was the one divider still carrying the half-width bug §101
   fixed for all the others.
   `!important` is what beats a non-important inline style, and is legitimate here
   for the same reason §101a's h3 rule is: the declaration is a constant emitted by
   tables.js on every render, never a computed value, and never animated. Doing it
   in CSS keeps all five JS modules byte-identical to live.
   Only border-top is forced — the inline left/right/bottom stay as authored. */
#monthlySalesTable tbody tr:last-child > * {
    border-top: 2px solid var(--navy) !important;
}

/* --- 2. Room tables: BUG C — un-stack Total from Average -------------------
   Identical fix to MS's `#monthlySalesTable th.total { position: static }`.
   Average stays pinned (it is the column you scan against); Total rejoins the
   scroll and becomes visible for the first time. */
#roomRevenueTable th.total,
#roomRevenueTable td.total {
    position: static;
}

/* --- 3. Room tables: demote the navy slab ---------------------------------
   MS_DESIGN_V1's finding, verbatim: a saturated navy column reads as a slab
   caging the data. Prominence comes from a tint + weight + a hairline. Same
   tokens as Monthly Sales — gray-50 for the now-static Total, gray-100 for
   Average.
   Average MUST stay OPAQUE: it is position:sticky, and a translucent fill lets
   the scrolling month cells show straight through it (caught on Monthly Sales in
   the browser, invisible in the CSS). --gray-100 is a solid hex, not an rgba. */
#roomRevenueTable tbody td.total,
#roomRevenueTable tbody td.average,
#roomOccupancyTable tbody td.average {
    color: var(--navy);
    border-left: 1px solid rgba(15, 31, 61, .12);
}
#roomRevenueTable tbody td.total {
    background: var(--gray-50);
}
#roomRevenueTable tbody td.average,
#roomOccupancyTable tbody td.average {
    background: var(--gray-100);
}

/* Row hover has to cross the pinned column, same as Monthly Sales: a tr's
   background paints UNDER its cells, and these cells now carry their own fill.
   #f4efe4 is --gold-100 pre-composited over white — the literal is deliberate,
   an rgba() would bleed the scrolling months through the sticky column. */
#roomRevenueTable tbody tr:hover > td.total,
#roomRevenueTable tbody tr:hover > td.average,
#roomOccupancyTable tbody tr:hover > td.average {
    background: #f4efe4;
}

/* --- 4. Property boundary: a 23px navy BAND becomes a 2px navy RULE --------
   Both room tables already separate properties — with a blank <tr> whose single
   <td colspan="17"> is painted `background: var(--navy)`. Measured: 23px tall,
   2147px wide, x11 in roomRevenue and x10 in roomOccupancy = ~253px of solid
   navy bands down one table. That is precisely the "thick saturated block" that
   MS_DESIGN_V1 demoted on Monthly Sales, still shipping here.
   The row is kept (it is emitted by charts/tables JS, which stays byte-identical)
   and collapsed into the rule it was always trying to be. Navy is retained
   deliberately: it is the same weight Monthly Sales gives its Net Revenue line —
   the heaviest boundary in the table. */
#roomRevenueTable tbody tr > td[colspan]:only-child,
#roomOccupancyTable tbody tr > td[colspan]:only-child {
    background: transparent;
    padding: 0;
    line-height: 0;
    font-size: 0;
    border: 0;
}

/* PROPERTY_AIR_V2 — air, not ink. V1 made this a 2px navy rule on the theory
   that "match Monthly Sales" meant reusing its heaviest boundary. Operator: too
   bold and heavy. Correct, and the reason is worth keeping:
   **FREQUENCY CHANGES WEIGHT.** Monthly Sales uses 2px navy ONCE, as the bottom
   line of a 7-row table. Repeated 11 times down a 120-row table the same rule
   stops reading as emphasis and becomes a ladder of bars. Porting a token
   faithfully is not the same as porting it correctly — the standard is the
   VOCABULARY (one light rule for a minor group, one strong signal for a major
   one), not the literal declaration.
   So the major boundary is now carried by whitespace, which separates without
   adding ink, plus a hairline to keep the edge crisp. Air is the quietest
   possible way to say "new group", and in a table this dense it reads instantly.

   The 23px was never anyone's design either: the JS emits the spacer as
   `<tr style="height: 8px">`, but `height` on a table row is a MINIMUM and the
   cell's inherited padding pushed it to 23px. `!important` on the <tr> (not the
   <td> — the height lives on the row) is what beats that inline value. */
#roomRevenueTable tbody tr:has(> td[colspan]:only-child),
#roomOccupancyTable tbody tr:has(> td[colspan]:only-child) {
    height: 10px !important;
    background: transparent !important;
}

/* --- 5. Room-type boundary: the light rule -------------------------------
   #roomRevenueTable is strictly Sales/ADR pairs (verified 12 deep), so every
   room type owns exactly 2 rows and its label cell carries rowspan="2". In the
   data area nothing marked those pairs — every row looked equally separated, so
   the grouping was legible ONLY in the far-left Room Type column.
   `:first-child` is what distinguishes a room-type start from a PROPERTY start:
   on a property's first row the property cell is first and the room-type cell is
   second; on every later room type the property cell is absorbed by its rowspan,
   so the room-type cell IS first. (My first guess — "first cell has rowspan =
   property start" — was wrong; rows 1,3,5,7... all matched it. The browser said
   so before I wrote a line of this.)
   #roomOccupancyTable gets NO rule here: its room types are one row each, so
   every row is already its own group and a rule per row is just noise. */
#roomRevenueTable tbody tr:has(> td[rowspan="2"]:first-child) > * {
    border-top: 2px solid rgba(15, 31, 61, .18);
}

/* The hairline that closes the gap and introduces the new property. It sits on
   the property's FIRST row rather than on the spacer, so the reading order is
   air -> rule -> property: the line leads into the group it belongs to instead
   of trailing the one that ended.
   It also has to win over the room-type rule above, or a property whose first
   room type is its ONLY room type (first cell rowspan="2" — property span ==
   room-type span, indistinguishable; e.g. "S-Presso Central Villa Five", row
   117) would draw both and smear. Higher specificity (1,2,4 vs 1,2,3) does that
   without !important.
   1px at .32 against the room-type rule's 2px at .18: nearly the same ink, so
   neither shouts. The 10px of air above is what makes this the stronger of the
   two boundaries — that is the whole point of V2. */
#roomRevenueTable tbody tr:has(> td[colspan]:only-child) + tr > *,
#roomOccupancyTable tbody tr:has(> td[colspan]:only-child) + tr > * {
    border-top: 1px solid rgba(15, 31, 61, .32);
}

/* ============================================================================
   STICKY_LEFT_V1 (staging) — the sticky label column: make it opaque, and let it
   hover. Two operator reports, one root cause.

   `.data-table td:first-child` is `position: sticky; left: 0`, so the month
   columns scroll UNDERNEATH it. Whether that column is readable depends entirely
   on it painting an OPAQUE background. Three different cells get this wrong:

     - base CSS paints it `white` (opaque — fine, but it blocks the row hover);
     - the JS writes an INLINE `rgba(15,31,61,0.05)` on the property cell;
     - the JS writes an INLINE `rgba(15,31,61,0.08)` on the ADR metric cell.

   The two translucent ones are the SAME bug §99/§100 caught on Monthly Sales'
   Average column, on the opposite edge of the table and never fixed: a
   translucent fill on a position:sticky cell lets the scrolling cells composite
   straight through it. VERIFIED IN THE BROWSER at scrollLeft=750 — the Property
   column renders "¥4,054,896 ¥4,262,707 ¥4,645,601" instead of the hotel name,
   and month digits overlap the "ADR" labels. **Confirmed identical on LIVE**
   (/analytics/, analytics.css) — pre-existing, not introduced by this work.
   ============================================================================ */

/* --- 1. THE CAUSE: `td:first-child` is not "the first column" ---------------
   Measured on #roomRevenueTable at scrollLeft=750 — every first child pins to
   the same x, but they are three DIFFERENT columns:

     row 1  "Hotel S-Presso Central"  col 0 (Property)  -> x=87..318   correct
     row 2  "ADR"                     col 2 (Metric)    -> x=87..161   WRONG
     row 3  "Two Bedroom Apartment"   col 1 (RoomType)  -> x=87..360   WRONG
     row 4  "ADR"                     col 2 (Metric)    -> x=87..161   WRONG

   Once the Property cell spans 20 rows, the next row's first CHILD is the
   RoomType cell and the one after is the Metric cell — the rowspan absorbs the
   column, but `:first-child` counts elements, not columns. All three match, all
   three pin to left:0, all three stack. Translucency didn't cause that; it just
   let you see the pile. Un-pin everything, then re-pin only true column 0.
   V1 of this made the cells opaque and called it fixed — that was the symptom;
   this is the cause. */
#roomRevenueTable tbody td:first-child,
#roomOccupancyTable tbody td:first-child {
    position: static;
}

/* Re-pin ONLY the Property cell. Keyed on STRUCTURE, not rowspan: a property
   block always begins at the first row of tbody or immediately after a spacer
   row. rowspan cannot express this — a property with one room type carries
   rowspan="2", identical to a room-type cell (row 117, "S-Presso Central Villa
   Five"), and #roomOccupancyTable row 59 carries none at all. Both are caught
   here; neither would be by `[rowspan]`.
   The Property cell keeps its own rowspan, so the hotel name stays pinned beside
   every row it owns. RoomType and Metric now scroll with their real columns. */
#roomRevenueTable tbody > tr:first-child > td:first-child,
#roomRevenueTable tbody > tr:has(> td[colspan]:only-child) + tr > td:first-child,
#roomOccupancyTable tbody > tr:first-child > td:first-child,
#roomOccupancyTable tbody > tr:has(> td[colspan]:only-child) + tr > td:first-child {
    position: sticky;
    left: 0;
    z-index: 2;
}

/* --- 2. The pinned cell — and ONLY it — must be opaque --------------------
   The JS writes an inline `rgba(15,31,61,0.05)` on the property cell. A sticky
   cell may not carry alpha or the scrolling columns composite through it (§99/
   §100's Average-column bug, opposite edge of the table, never fixed). Verified
   at scrollLeft=750: the Property column rendered "¥4,054,896 ¥4,262,707"
   instead of the hotel name. Confirmed identical on LIVE — pre-existing.
   Pre-composited over white, the same technique as #f4efe4 (= --gold-100 over
   white) elsewhere in this file: rgba(15,31,61,.05) over #fff = rgb(243,244,245).
   Same colour, zero alpha. !important because the source is inline.
   Scoped by the same structural selector, so the ADR cell's inline .08 tint and
   the RoomType cell's white are left exactly as they were — now that neither is
   sticky, translucency there is harmless. */
#roomRevenueTable tbody > tr:first-child > td:first-child,
#roomRevenueTable tbody > tr:has(> td[colspan]:only-child) + tr:not(:last-child) > td:first-child,
#roomOccupancyTable tbody > tr:first-child > td:first-child,
#roomOccupancyTable tbody > tr:has(> td[colspan]:only-child) + tr:not(:last-child) > td:first-child {
    background-color: rgb(243, 244, 245) !important;
}

/* --- 2. The row hover has to cross the sticky label column -----------------
   Monthly Sales already does this via `#monthlySalesTable tbody tr:hover > th`.
   Every OTHER table kept the white fill, so the hover died at the label cell —
   visible on OTA Commission, where the row tints but "Agoda.com" stays white.
   Instance fixed, bug left: the same shape as everything else in §103.

   `:not([rowspan])` is deliberate, not an oversight. A rowspan cell belongs to
   ONE row but covers many, so tinting it on that row's hover paints a 20-row-tall
   highlight for a 1-row hover (the property cell spans up to 20). Grouping
   columns therefore stay neutral and only the hovered row's OWN cells light up —
   consistent for every row, which a partial exception would not be.
   !important beats the inline rgba on the ADR cell. */
/* `:not(.an-total)` added by TOTAL_TINT_V1. #f4efe4 is --gold-100 over WHITE, right for a normal
   row whose label cell is white — but a summary row's cells sit on --gray-100, where the same wash
   lands on #e6e4dc. Without this exclusion the label cell took #f4efe4 (this rule is !important and
   more specific, 0,4,3 vs 0,3,2) while the rest of the row took #e6e4dc: a two-tone hovered row —
   the same partial band, arriving from the opposite direction. Excluding is honest; escalating to a
   bigger !important would just be a louder version of the same collision. */
.data-table tbody tr:not(.an-total):hover > td:first-child:not([rowspan]) {
    background-color: #f4efe4 !important;
}

/* The spacer row is `td:first-child` too, so it would pick up both rules above
   and reappear as a 10px tinted bar on hover. It is air; keep it air. */
#roomRevenueTable tbody tr:has(> td[colspan]:only-child) > td,
#roomOccupancyTable tbody tr:has(> td[colspan]:only-child) > td,
#roomRevenueTable tbody tr:hover:has(> td[colspan]:only-child) > td,
#roomOccupancyTable tbody tr:hover:has(> td[colspan]:only-child) > td {
    background-color: transparent !important;
}

/* --- 3. The 4px navy spine on the property cell ---------------------------
   The JS writes an INLINE `border-left: 4px solid rgb(15,31,61)` on every
   property cell of both room tables — a saturated navy bar down the left edge of
   each property block. No other .data-table has any left border on its cells
   (#commissionDetailTable, the reference: `0px none`); the card's own rounded
   edge is what bounds them. These two were the odd ones out.

   It is also redundant now. That spine was the only thing grouping properties
   visually before PROPERTY_AIR_V2; the 10px air gap + hairline now say exactly
   the same thing, so keeping it means saying it twice — once quietly and once in
   4px of navy. Same MS_DESIGN_V1 finding as everywhere else in this thread: a
   saturated block reads loud, and prominence should come from structure, not ink.

   !important because the source is inline. Scoped to :first-child on the two room
   tables. Other cells DO carry a left border — 108+108 on roomRevenue and 54 on
   roomOccupancy — but those are the deliberate `.12` hairlines this file puts on
   Total/Average, and none of them is a :first-child, so this rule cannot reach
   them. Verified after the change: first-child cells with any left border = 0 on
   both tables, and the Total/Average hairlines still measure 1px rgba(...,.12).
   (An earlier draft of this comment claimed "no other cell has a left border".
   That was false and the count caught it — §100's rule: a false comment is how a
   bug survives review.) */
#roomRevenueTable tbody td:first-child,
#roomOccupancyTable tbody td:first-child {
    border-left: 0 !important;
}

/* ==========================================================================
   ICON_SYSTEM_V1 (staging) — the page's emoji become stroked currentColor SVGs.

   WHY PSEUDO-ELEMENTS, NOT <svg> CHILDREN. ui.js:489 runs, on every language toggle:
       document.querySelectorAll('h2, h3, h4, h5').forEach(h => {
         const original = h.getAttribute('data-original-text') || h.textContent.trim();
         h.textContent = t(original);   // <-- destroys ALL child nodes
       });
   An <svg> inside a heading is wiped the first time anyone touches JP/EN. A ::before is not a
   child node, so textContent cannot reach it. Same shape as §102: createChart() destroys and
   rebuilds its instances, so the display had to be a Chart.js plugin, not a one-shot write.

   WHY core.js HAD TO CHANGE. That same loop looks up `h.textContent.trim()` in TRANSLATIONS by
   EXACT match, and the keys carried the emoji ('💳 WHAT EACH METRIC MEANS:'). Removing the emoji
   from the HTML alone would silently kill the JA translation of those headings; leaving it in
   means the toggle paints it back over the icon. Neither is reachable from CSS — so the 9 keys
   and their JA values were updated in js/analytics-next/core.js. LIVE js/analytics/core.js is
   UNTOUCHED: staging loads /js/analytics-next/*.js, live loads /js/analytics/*.js.

   MASK, NOT background-image. background-image cannot inherit ink. mask + background-color:
   currentColor paints the icon in whatever colour owns the text, so an icon can never drift
   from its token — the same reason SPARK_V1 strokes with currentColor and PDF_SVG_V1 uses
   stroke="currentColor". It is also why .collapse-icon's existing hover rules (navy, and #fff
   under the §101a :has() rule) keep working with no new colour values introduced.

   stroke-width is 1.9, matching PDF_SVG_V1 on the 15 PDF buttons — deliberately NOT a new
   weight. Sized in em, so each icon tracks the font-size of the heading that owns it.

   ENCODING: SVG attributes use single quotes and the payload is percent-encoded. Keeping `"`
   unencoded inside url("...") closes the CSS string at xmlns=" — every rule then parses as
   invalid and the icons render NOTHING, silently. That shipped in the first attempt and was
   caught only by asserting mask-image != none on all 13 in the browser.
   ========================================================================== */

.an-ico::before {
    content: "";
    display: inline-block;
    width: 1em;
    height: 1em;
    margin-right: .45em;
    vertical-align: -.14em;
    background-color: currentColor;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: center;   mask-position: center;
    -webkit-mask-size: contain;      mask-size: contain;
    -webkit-mask-image: var(--an-ico); mask-image: var(--an-ico);
}

.an-ico-card { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Crect%20x='2.9'%20y='5.4'%20width='18.2'%20height='13.2'%20rx='2'/%3E%3Cpath%20d='M2.9%209.8h18.2'/%3E%3Cpath%20d='M6.3%2015.1h3.4'/%3E%3C/svg%3E"); }
.an-ico-sync { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M20.2%2012a8.2%208.2%200%200%201-14.1%205.7'/%3E%3Cpath%20d='M3.8%2012A8.2%208.2%200%200%201%2017.9%206.3'/%3E%3Cpath%20d='M18.4%203v3.6h-3.6'/%3E%3Cpath%20d='M5.6%2021v-3.6h3.6'/%3E%3C/svg%3E"); }
.an-ico-list { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Crect%20x='5.4'%20y='4.9'%20width='13.2'%20height='16.2'%20rx='2'/%3E%3Cpath%20d='M9.4%202.9h5.2v3.4H9.4z'/%3E%3Cpath%20d='M8.9%2011.6h6.2'/%3E%3Cpath%20d='M8.9%2015.4h4.2'/%3E%3C/svg%3E"); }
.an-ico-dot { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M12%204.6%2019.4%2012%2012%2019.4%204.6%2012z'/%3E%3C/svg%3E"); }
.an-ico-hotel { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Crect%20x='4.4'%20y='2.9'%20width='15.2'%20height='18.2'%20rx='1.6'/%3E%3Cpath%20d='M8.6%207.6h1.4'/%3E%3Cpath%20d='M14%207.6h1.4'/%3E%3Cpath%20d='M8.6%2011.6h1.4'/%3E%3Cpath%20d='M14%2011.6h1.4'/%3E%3Cpath%20d='M10%2021.1v-4.4h4v4.4'/%3E%3C/svg%3E"); }
.an-ico-chart { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3.4%203.2v17.4h17.4'/%3E%3Cpath%20d='M7.8%2017.2v-4.6'/%3E%3Cpath%20d='M12%2017.2V8.4'/%3E%3Cpath%20d='M16.2%2017.2v-6.6'/%3E%3C/svg%3E"); }

/* The former 🔹 bullets sit at h5 size; hold them smaller so they read as a marker rather than
   compete with the OTA name they introduce. */
.an-ico-dot::before { width: .62em; height: .62em; vertical-align: .04em; }

/* The collapse chevron. The span keeps its existing `transform: rotate(-90deg)` when collapsed
   and its existing hover colour rules — the mask inherits both, so neither the collapse
   behaviour nor §101a's :has() hover fix changes. font-size:18px already sits on .collapse-icon,
   so 1em = 18px, the size the ▼ glyph occupied. */
.collapse-icon::before {
    content: "";
    display: block;
    width: 1em;
    height: 1em;
    background-color: currentColor;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: center;   mask-position: center;
    -webkit-mask-size: contain;      mask-size: contain;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M5.8%209.2%2012%2015.4l6.2-6.2'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M5.8%209.2%2012%2015.4l6.2-6.2'/%3E%3C/svg%3E");
}


/* ==========================================================================
   ICON_SYSTEM_V2 — the icons I wrongly reported as unreachable.

   I told the operator the app drawer was "a shared component generated outside the analytics
   modules, out of scope". FALSE. It is built by ui.js:311 `item(icon, name)` — one of our own
   staging files. My grep for the literal 🏢 returned 0 because the source writes ES6 code-point
   escapes: item('\u{1F3E2}', p). The literal never appears in the file.
   That is the §105 lesson repeated one hour after logging it: A GREP PROVES WHAT MATCHED,
   NEVER WHAT EXISTS. When a rendered glyph has no source in any file, the source is ENCODING it.
   Search by CODEPOINT and by the element class, not by the character.

   Likewise the forecast status glyphs: I called them "data, and JS is off-limits". The JS
   constraint had already been lifted, and the word Below/Exceeding IS the data — the dot is
   redundant colour. I also missed two states entirely (🟡 On Track, 📊 Forecast) and the three
   statusIcon* glyphs in the metric subtitles, because my grep list only had 🔴 and 🟢.
   ========================================================================== */

.an-ico-globe { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9.1'/%3E%3Cpath%20d='M2.9%2012h18.2'/%3E%3Cpath%20d='M12%202.9a14%2014%200%200%201%200%2018.2a14%2014%200%200%201%200-18.2'/%3E%3C/svg%3E"); }
.an-ico-building { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Crect%20x='4.4'%20y='2.9'%20width='15.2'%20height='18.2'%20rx='1.6'/%3E%3Cpath%20d='M8.6%207.3h1.4'/%3E%3Cpath%20d='M14%207.3h1.4'/%3E%3Cpath%20d='M8.6%2011.4h1.4'/%3E%3Cpath%20d='M14%2011.4h1.4'/%3E%3Cpath%20d='M8.6%2015.5h1.4'/%3E%3Cpath%20d='M14%2015.5h1.4'/%3E%3C/svg%3E"); }
.an-ico-bolt { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M12.9%202.9%204.6%2013.4h6.1l-1.6%207.7%208.3-10.5h-6.1z'/%3E%3C/svg%3E"); }
.an-ico-search { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='10.6'%20cy='10.6'%20r='7.1'/%3E%3Cpath%20d='M15.8%2015.8%2021%2021'/%3E%3C/svg%3E"); }

/* An icon with no label beside it (the floating drawer button) carries no gap. */
.an-ico-solo::before { margin-right: 0; }

/* --- Status dots (forecast table + metric subtitles) ----------------------
   Were 🟢 / 🔴 / 🟡 / 📊 colour-emoji bitmaps: they ignore the page ink, render differently per
   OS, and cannot be sized. Now a masked circle painted with currentColor, tinted by the base.css
   status tokens. Decorative by design — the adjacent word (Exceeding / Below / On Track /
   Forecast) carries the meaning, and that TEXT is untouched so its data-original-text
   translation keys still match. */
.an-dot::before {
    content: "";
    display: inline-block;
    width: .72em;
    height: .72em;
    margin-right: .4em;
    vertical-align: -.02em;
    background-color: currentColor;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: center;   mask-position: center;
    -webkit-mask-size: contain;      mask-size: contain;
    -webkit-mask-image: var(--an-ico); mask-image: var(--an-ico);
}
.an-dot-up, .an-dot-down, .an-dot-flat { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='12'%20cy='12'%20r='6.4'%20fill='%23000'%20stroke='none'/%3E%3C/svg%3E"); }
.an-dot-forecast { --an-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M14%202H6.5A1.5%201.5%200%200%200%205%203.5v17A1.5%201.5%200%200%200%206.5%2022h11a1.5%201.5%200%200%200%201.5-1.5V7z'/%3E%3Cpath%20d='M14%202v5h5'/%3E%3Cpath%20d='M12%2011v6'/%3E%3Cpath%20d='M9.5%2014.5%2012%2017l2.5-2.5'/%3E%3C/svg%3E"); }
.an-dot-up       { color: var(--success); }
.an-dot-down     { color: var(--error); }
.an-dot-flat     { color: var(--warning); }
.an-dot-forecast { color: var(--navy); opacity: .55; }

/* --- PDF button icon ------------------------------------------------------
   LATENT BUG this fixes: downloadChartPDF()'s finally block does
       btn.textContent = '📊 PDF';
   (ui.js:1361). TOOLBAR_ROLLOUT_V1 swapped the button to inline SVG at load, but textContent
   DESTROYS that svg — so downloading a PDF once reverted that one button to the emoji, and the
   normaliser never re-ran (it is guarded by `if (!b.querySelector("svg"))` and fires once at
   load). The button stayed emoji until a reload, while its 14 siblings stayed SVG.
   Moving the icon to ::before makes it immune: a pseudo-element is not a child, so no
   textContent write can reach it — the same property that makes the heading icons survive the
   language toggle. The normaliser no longer needs to swap anything. */
.chart-pdf-btn::before {
    content: "";
    display: inline-block;
    width: 1.05em;
    height: 1.05em;
    margin-right: .4em;
    vertical-align: -.16em;
    background-color: currentColor;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: center;   mask-position: center;
    -webkit-mask-size: contain;      mask-size: contain;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M14%202H6.5A1.5%201.5%200%200%200%205%203.5v17A1.5%201.5%200%200%200%206.5%2022h11a1.5%201.5%200%200%200%201.5-1.5V7z'/%3E%3Cpath%20d='M14%202v5h5'/%3E%3Cpath%20d='M12%2011v6'/%3E%3Cpath%20d='M9.5%2014.5%2012%2017l2.5-2.5'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M14%202H6.5A1.5%201.5%200%200%200%205%203.5v17A1.5%201.5%200%200%200%206.5%2022h11a1.5%201.5%200%200%200%201.5-1.5V7z'/%3E%3Cpath%20d='M14%202v5h5'/%3E%3Cpath%20d='M12%2011v6'/%3E%3Cpath%20d='M9.5%2014.5%2012%2017l2.5-2.5'/%3E%3C/svg%3E");
}


/* ============================================================================
   MS_NET_FILL_V1 (staging) — the bottom line takes the summary tint.

   Operator asked for the Net Revenue row to carry the Average column's colour.
   It is the standard summary-row convention, and it is coherent here rather than
   arbitrary: Net Revenue × Average is itself a meaningful cell (average monthly
   net revenue), so tinting both axes and letting them meet reads as one summary
   frame down the right edge and along the bottom — not as two unrelated signals
   that happen to share a value.

   WHY THIS IS ON THE CELLS, NOT THE <tr>. A tr's background paints UNDERNEATH its
   cells, and this row has four: the label th is opaque white and sticky, the
   month cells are transparent, Total is --gray-50, Average is already --gray-100.
   `tr { background }` would tint ONLY the transparent month cells and leave a
   partial band with white and gray-50 chunks in it — the identical failure as the
   row hover (a tr tint that died at both ends) and as the §103 half-width rules.
   Every cell has to be told. */
#monthlySalesTable tbody tr:last-child > * {
    background: var(--gray-100);
}

/* Total flattens from --gray-50 to --gray-100 on this row only. That is intended:
   on the summary row the whole band should read as one unit, and a lighter Total
   cell sitting inside it would look like a hole rather than a distinction. The
   Total/Average split still reads on all six rows above. */

/* HOVER. The cells above now paint their own opaque fill, so `.data-table tbody
   tr:hover { background: --gold-100 }` (a tr rule) can no longer reach this row —
   it would hover as the only dead row in the table.
   #f4efe4 is NOT reusable here: it is --gold-100 rgba(160,122,34,.12) composited
   over WHITE. Over --gray-100 the same wash lands on rgb(230,228,220). Computed,
   not eyeballed — the same arithmetic reproduces #f4efe4 exactly from white, which
   is what validates the method. Opaque because these cells are position:sticky.
   The value differs from every other row BECAUSE the base differs; the behaviour
   (one gold-100 wash) is identical, which is what consistency actually means. */
#monthlySalesTable tbody tr:last-child:hover > * {
    background: #e6e4dc;
}

/* ============================================================================
   TOTAL_TINT_V1 (staging) — the summary row of the other seven tables takes the
   same tint as Monthly Sales' Net Revenue row.

   Daily Summary, OTA Commission, Room Sales by Room Type, Room Occupancy by Room
   Type, Market Pricing, Forecasting, Accommodation Tax — all seven already marked
   their summary row with an INLINE `background-color: rgba(15,31,61,.1)` + bold,
   written from seven places in tables.js. That inline fill is now a CLASS
   (`an-total`), set at exactly those seven sites. Same signal, but a class can be
   styled, themed and hovered; an inline fill can only be beaten with !important.

   WHY A CLASS AND NOT `tr:last-child` PER TABLE. "Last row" is wrong for two of
   them, and dangerously wrong for one:
     - Forecasting's summary is "S-Presso Hotels (Total)" — the FIRST row.
     - Market Pricing's is "Your Hotel Group" — first ONLY because it is ranked #1
       by ADR. tables.js:1362 marks it with `if (hotel.isSpresso)`. If a competitor
       ever out-priced you, a :first-child rule would tint THEIR row and quietly
       mislabel a competitor as your hotel. The JS already knows which row is
       yours; keying on rank instead would have been a real defect, not a nit.

   WHY `> *` AND NOT THE <tr>. A tr's background paints UNDERNEATH its cells, and
   these tables have `.data-table td:first-child { background: white }` (the sticky
   label column). A tr rule tints every cell EXCEPT the first — a partial band.
   That is now the fourth appearance of this exact shape (§103 half-width rules,
   §104 row hover, §106 MS_NET_FILL). Tell the cells.

   Specificity: `.data-table td:first-child` is (0,2,1). `.an-total > *` alone is
   (0,1,0) and would LOSE to it, leaving a white first cell. `.data-table tbody
   tr.an-total > *` is (0,2,2) and wins on merit — no !important needed.
   ============================================================================ */
.data-table tbody tr.an-total > * {
    background: var(--gray-100);
}

/* These rows never responded to hover: the inline fill on the <tr> beat
   `.data-table tbody tr:hover`, so the summary row of all seven tables was
   hover-dead. Now the cells carry the fill, so the tr-level hover still cannot
   reach them — but this can. #e6e4dc is --gold-100 composited over --gray-100
   (NOT the #f4efe4 used elsewhere, which is composited over white). Same wash,
   darker base, same reasoning as MS_NET_FILL_V1. */
.data-table tbody tr.an-total:hover > * {
    background: #e6e4dc;
}

/* ============================================================================
   ROWSPAN_HOVER_V2 (staging) — V1's model was WRONG, disproved by measurement.

   V1 claimed: "make a spanned cell transparent and each <tr> shows through its
   own band, so the cell displays exactly the hovered row's 1-row slice."
   **That is false.** Painted a Sales row red and its ADR row blue and looked: the
   rowspan=2 RoomType cell rendered ENTIRELY RED — the row it BELONGS to paints
   behind its FULL span, not one band per row. The ADR row's own cells were blue,
   but its half of the shared cell was red.

   Consequences, both real:
     - hovering Sales flooded the whole 2-row RoomType cell gold while the ADR
       row's other cells stayed striped — the stepped, broken look the operator
       reported;
     - hovering ADR could never tint that cell at all, because the cell shows the
       SALES row's background, not its own.

   **So a rowspan cell cannot participate in a per-row hover. At all.** Not by
   transparency, not by any selector, not by JS — one cell cannot paint two states.
   Same conclusion the Property column reached in §107a, now proven for the general
   case rather than argued from the tree.

   Which is exactly why Room Occupancy "works well" and Room Sales did not: the
   only spanned column in Occupancy is Property. Its Room Type is one row per cell,
   so it takes the row tint like any normal cell.

   So: transparent for cells that own exactly ONE row (they take the row's tint
   correctly), and an explicit opaque fill for spanned cells (so they never take a
   hover they cannot express). The fill tracks the pair's zebra state, or the
   spanned cell would be the one thing not striped with its own rows.
   ============================================================================ */

/* Single-row first cells: transparent, so the row's background — stripe or hover —
   shows through. The ADR cell keeps its inline rgba(15,31,61,.08) at rest (inline
   outranks this) and still takes #f4efe4 !important on hover. */
#roomRevenueTable tbody td:first-child:not([rowspan]),
#roomOccupancyTable tbody td:first-child:not([rowspan]) {
    background-color: transparent;
}

/* PAIR_HOVER_V1 supersedes V2's opaque spanned cell. V2 forced the RoomType cell opaque to stop
   the owning row's hover flooding both rows of the pair. Under the pair model that flood is exactly
   what we want — the room type IS the unit — so the cell goes back to transparent and the fill it
   inherits across its full span is correct by construction. The rowspan stops being a problem to
   work around and becomes the mechanism.
   The Property cell (also `td:first-child[rowspan]`) is unaffected: it keeps its
   rgb(243,244,245) !important from STICKY_LEFT_V2, so an 18-row property block can never flood. */
#roomRevenueTable tbody td:first-child[rowspan] {
    background-color: transparent;
}

/* ============================================================================
   ZEBRA_CLASS_V1 (staging) — the real reason the room-table hover never covered
   the row.

   tables.js:732 / :1145 wrote the zebra stripe as an INLINE style on the <tr>:
       const bgColor = roomIndex % 2 === 0 ? 'rgba(255,255,255,.9)' : 'rgba(248,250,252,.9)';
       revenueRow.style.backgroundColor = bgColor;
   An inline background on the <tr> OUTRANKS `.data-table tbody tr:hover`, so those
   rows NEVER turned gold — on either table, since long before this work. The month
   cells are transparent, so they showed that inline stripe unchanged on hover, and
   the ONLY cells that lit up were the ones with their own !important cell rule
   (the label column, Average). That is precisely the "hover covers half the row"
   the operator kept seeing — and why my previous pass only moved WHICH cells were
   stranded instead of fixing it. The cause was one level up the whole time.

   Now a class, so the cascade works and the <tr> hover reaches every transparent
   cell in the row for free.

   The stripe stays keyed on roomIndex (not :nth-child) because it groups a room
   type's Sales+ADR PAIR — nth-child would alternate per ROW and cut every pair in
   half. That is a real distinction in the data, not a detail.

   --gray-50 is the composited equivalent of the old rgba(248,250,252,.9) over
   white (rgb(249,250,252) vs #f8fafc — a rounding apart) and is already the token
   this file uses for a light table tint.
   ============================================================================ */
.data-table tbody tr.an-zebra {
    background-color: var(--gray-50);
}

/* The hover MUST outrank the stripe. `tr.an-zebra` is (0,2,2) and so is the base
   `.data-table tbody tr:hover` — a tie, and the stripe is declared later, so it
   would win and swallow the hover on every other room type. `tr.an-zebra:hover`
   is (0,3,2) and settles it on specificity rather than on source order. */
.data-table tbody tr.an-zebra:hover {
    background-color: var(--gold-100);
}

/* ============================================================================
   PAIR_HOVER_V1 (staging) — a room type hovers as ONE unit.

   Operator: "when hovering sales or adr, make the room type entirely hover
   instead of each one only." This is the right model and it DISSOLVES the rowspan
   problem rather than working around it. Sales and ADR are two facets of one room
   type; the room type is the unit a reader actually scans. And once both rows of
   the pair light together, the spanned RoomType cell painting across both is
   correct BY CONSTRUCTION — the exact behaviour §107c had to suppress becomes the
   intent. V2's opaque spanned cell is retired.

   The two combinators, and why the class is needed:
     `tr:hover + tr.an-adr`      Sales hovered -> tint its ADR partner.
     `tr:has(+ tr.an-adr:hover)` ADR hovered   -> tint its Sales partner (CSS has
                                 no previous-sibling combinator; :has() supplies it).
   `.an-adr` is what stops this leaking across a pair boundary: a bare
   `tr:hover + tr` fired from an ADR row would tint the NEXT room type's Sales row.

   Specificity: these are (1,2,3) / (1,3,3), so they outrank `tr.an-zebra` (0,2,2)
   and the partner row lights even inside a striped pair.

   The Property cell cannot flood the same way — it is opaque (sticky ⇒ must be),
   so an 18-row property block never lights from a 1-row hover. Only the 2-row
   RoomType cell participates, which is precisely the intended unit.
   ============================================================================ */
#roomRevenueTable tbody tr:hover + tr.an-adr,
#roomRevenueTable tbody tr:has(+ tr.an-adr:hover) {
    background-color: var(--gold-100);
}

/* The partner row's own label cell must match the band. The hovered row gets
   #f4efe4 from STICKY_LEFT_V1, but the PARTNER never matches `tr:hover`, so its
   "ADR"/"Sales" metric cell would keep its inline rgba(15,31,61,.08) and composite
   ~rgb(226,222,215) — visibly darker than the rest of the band. !important because
   that tint is inline. `:not([rowspan])` keeps the spanned RoomType cell out: it is
   transparent on purpose now. */
#roomRevenueTable tbody tr:hover + tr.an-adr > td:first-child:not([rowspan]),
#roomRevenueTable tbody tr:has(+ tr.an-adr:hover) > td:first-child:not([rowspan]) {
    background-color: #f4efe4 !important;
}

/* ============================================================================
   NUM_ALIGN_V1 — carry MS_DESIGN_V1 item 3 to the other numeric tables.

   THE GAP. MS_DESIGN_V1 (~line 1223) right-aligns + tabular-nums Monthly Sales,
   scoped to #monthlySalesTable. The `.data-table` base is `text-align: left`
   (~line 375), so every OTHER table still renders its yen left-aligned in
   proportional figures: ¥37,861,300 / ¥5,522,685 / ¥412,900 all start at the same
   left edge, so magnitude has to be READ instead of seen. MS's own rationale —
   "figures are compared DOWN a column, so they align on the digit" — is not a
   property of Monthly Sales, it is a property of a column of numbers. Same shape
   as the DIVIDER_STANDARD_V1 sweep: an ID-scoped fix repaired the instance and
   left the bug.

   NOTHING BUT ALIGNMENT MOVES. No value, format, class or DOM changes; the JS
   modules stay byte-identical. tabular-nums is a font-feature toggle (fixed
   advance width per digit) — without it the digit columns still wobble because
   'Noto Sans' renders `1` narrower than `8`, so right-align alone is only half
   the fix.

   LABEL COLUMNS STAY LEFT. Each table below is targeted by its own structure,
   never by a shared `.data-table` rule: the four flat tables key on
   `:not(:first-child)`, and #roomRevenueTable CANNOT — see its block.
   #roomOccupancyTable is deliberately absent: it centres its % cells with an
   INLINE style (tables.js ~1146/1151), which beats the stylesheet, and centred
   percentages are already legible. Leaving it needs no !important; forcing it
   would.
   ============================================================================ */

/* --- Flat tables: column 1 is the label, every other column is numeric -------
   Verified cell-by-cell in tables.js: dailySummary `<td>${day}</td>` + 7 numeric;
   commissionDetail channel + 8; taxDetail day + 6; forecast property + 3 yen.
   Forecast stops at nth-child(4) on purpose — its columns 5 and 6 are a
   confidence bar and a status word, not figures.
   `:not([colspan])` keeps the "No data available" placeholder rows centred as
   authored. The header cells come along so each label sits over its own digits. */
#dailySummaryTable thead th:not(:first-child),
#dailySummaryTable tbody td:not(:first-child):not([colspan]),
#commissionDetailTable thead th:not(:first-child),
#commissionDetailTable tbody td:not(:first-child):not([colspan]),
#taxDetailTable thead th:not(:first-child),
#taxDetailTable tbody td:not(:first-child):not([colspan]),
#forecastTable thead th:nth-child(n+2):nth-child(-n+4),
#forecastTable tbody td:nth-child(n+2):nth-child(-n+4):not([colspan]) {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* --- #roomRevenueTable: keyed on STRUCTURE, because position lies -------------
   The documented rowspan trap, hit again. This table's leading label cells vary
   per row: a property-start row emits property(rowspan=N) + roomType(rowspan=2) +
   metric = 3 labels; the next room type emits roomType(rowspan=2) + metric = 2;
   an ADR row emits metric only = 1. So `:not(:first-child)` — and any nth-child
   count — would right-align a LABEL on two rows out of three. `td:first-child` is
   not the first column once a rowspan absorbs it.
   What IS stable is what each cell carries: the two span cells have [rowspan], the
   Sales/ADR metric cell has [data-original-text] (the i18n hook), the property
   spacer has [colspan]. Excluding those three leaves exactly the month cells plus
   .total and .average — the yen, and only the yen.

   The HEADER needs a different rule from the body, and `:not(:first-child)` is
   WRONG here — caught in the browser, invisible in the cell measurements. The
   thead is flat (no rowspan): Property | Room Type | Metric | 01..12 | Total |
   Average, so columns 2 and 3 are LABEL headers whose body cells are left-aligned.
   Right-aligning them pushed "Room Type" hard against its right edge while
   "Deluxe Suite" sat at the left of the same column. Numeric headers start at 4. */
#roomRevenueTable thead th:nth-child(n+4),
#roomRevenueTable tbody td:not([rowspan]):not([data-original-text]):not([colspan]) {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* ===================================================================
   MOBILE_TOOLBAR_V1 (2026-07-20)
   .ms-toolbar's Timeline Basis control is a segmented pair — joined
   border, flex-wrap:nowrap, white-space:nowrap — right on desktop and
   unreadable on a phone: labels are 218px ("Check-in Based (Operations)")
   + 210px ("Check-out Based (Finance)") = a 430px control inside a 286px
   toolbar. It did NOT register as horizontal overflow — .section is
   overflow-x:hidden, so it simply CLIPPED the second option off screen.
   That is why this needed measuring rather than eyeballing scrollWidth.
   The labels live in the HTML, so the only CSS lever is to let them wrap.

   PLACED AT END OF FILE ON PURPOSE. First attempt sat at ~line 1000 and
   silently lost to .ms-toolbar .timeline-option at line ~2111 — identical
   (0,2,0) specificity, so LATER SOURCE ORDER WINS. Monthly Sales appeared
   fixed only because its #monthlySalesSection companion is (1,2,0) and
   beats source order; the other 6 toolbars stayed clipped at 303>284.
   Two different tie-breakers, one of them invisible. Do not move this up.
   =================================================================== */
@media (max-width: 768px) {
  .ms-toolbar .timeline-basis-container {
    flex-wrap: wrap;
    width: 100%;
  }
  .ms-toolbar .timeline-options,
  #monthlySalesSection .ms-toolbar .timeline-options { width: 100%; }
  /* The ID-scoped companion is REQUIRED: #monthlySalesSection .timeline-option
     is (1,1,0) and outranks a (0,2,0) class rule no matter how late it appears.
     Same shape as the .data-table lesson — an ID-scoped rule is still live even
     when a newer, more "specific-looking" class rule exists. */
  .ms-toolbar .timeline-option,
  #monthlySalesSection .ms-toolbar .timeline-option {
    flex: 1 1 0;
    min-width: 0;
    white-space: normal;      /* the whole point — override the desktop nowrap */
    text-align: center;
    justify-content: center;
    padding: 8px 6px;
    font-size: 0.82rem;
    line-height: 1.25;
  }
  /* selects in the same bar go full-width rather than sitting in a ragged row */
  .ms-toolbar .filter-controls { width: 100%; }
  .ms-toolbar .filter-controls select { flex: 1 1 140px; min-width: 0; }
}

/* ===================================================================
   MOBILE_TABLE_SCROLL_V1 (2026-07-20)
   Every .data-table sits in a .table-container that is ALREADY
   overflow-x:auto — but .data-table is width:100%, so the table never
   exceeds the container and there is nothing to scroll. It compresses
   instead: 12-17 columns crushed into ~490px. Natural widths measured
   at a 390px viewport: roomRevenueTable 2059px, monthlySalesTable
   1658px, roomOccupancyTable 1348px, commissionDetailTable 1279px.

   width:auto lets the table take its natural width and the existing
   container scrolls it. min-width:100% keeps short tables (competitors,
   2 rows) filling the panel instead of shrinking to content.

   The sticky columns this relies on ALREADY EXIST and are already
   correct — no new pinning is added here:
     .data-table th/td:first-child  -> sticky left  (label column)
     .data-table .total/.average    -> sticky right
   Critically, #roomRevenueTable / #roomOccupancyTable already UN-pin
   tbody td:first-child and re-pin only true column 0 keyed on STRUCTURE
   (a property block starts at the first tbody row or after a spacer),
   because rowspan absorbs the column and :first-child counts ELEMENTS,
   NOT COLUMNS. Verified by scrolling a container 400px at 1280px: only
   "Hotel S-Presso" computed sticky; the rowspan-displaced "ADR" and
   "Two Bedroom Apartment" cells computed static, as intended. Enabling
   scroll is what would have EXPOSED that bug had it not been fixed —
   do not weaken those rules.
   =================================================================== */
@media (max-width: 768px) {
  .data-table {
    width: auto;
    min-width: 100%;
  }
  /* headers stay on one line — a wrapped 3-line header row on a phone costs
     more vertical space than the horizontal scroll it saves */
  .data-table thead th { white-space: nowrap; }
}

/* MOBILE_TABLE_LABELCOL_V1 (2026-07-20) — cap the pinned label column.
   Enabling scroll above exposed a second problem that only appears once a
   table is genuinely wider than its container: the sticky left column sized
   to its content and ate the screen. Measured at 390px BEFORE this rule —
   monthlySalesTable 221px pinned inside a 286px container, roomRevenueTable /
   roomOccupancyTable / forecastTable 215px inside 244px. That is ~88% of the
   viewport frozen, leaving 25-65px of scrollable data: technically scrolling,
   practically useless. The cap + wrap trades one taller label for a usable
   data area. Applies to th (monthlySales is the only table with a th label
   column) and td (everything else, incl. the structurally re-pinned property
   cell in the two room tables — same element, so this reaches it). */
@media (max-width: 768px) {
  .data-table th:first-child,
  .data-table td:first-child {
    min-width: 96px;
    max-width: 120px;
    white-space: normal;
    /* break-word, NOT anywhere. `overflow-wrap: anywhere` participates in
       intrinsic sizing — it drops min-content width to about one character, so
       the column collapsed to 38-43px and the labels became a tall thin ribbon.
       break-word wraps identically at render time but leaves min-content alone,
       so the 96px floor is what actually decides the width. */
    overflow-wrap: break-word;
  }
}

/* MOBILE_GUTTERS_V1 (2026-07-20) — reclaim horizontal space for table data.
   Measured at a 390px viewport: the padding stack around a table container was
   body 20+20, .section 20+20, .collapsible-content 20+20 = 120px, or 31% of the
   screen, before a single column was drawn. Table containers were 244-286px
   wide and, once the label column is pinned, only 148px of that was scrollable
   data. Tightening the three gutters returns ~52px to the data area.
   Only the HORIZONTAL padding of .collapsible-content is touched: its shorthand
   is `padding: 0 20px` and the vertical half participates in the open/close
   max-height+padding transition, so setting the shorthand here would animate
   wrong. body max-width:98% -> 100%: the 2% inset costs ~8px and buys nothing
   on a phone, where the bezel already provides the margin. */
@media (max-width: 768px) {
  body {
    padding: 10px;
    max-width: 100%;
  }
  .section { padding: 12px; }
  .collapsible-content {
    padding-left: 12px;
    padding-right: 12px;
  }
}
