@ottotheagent/mcp-card-assets 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @ottotheagent/mcp-card-assets
2
2
 
3
- **Created**: 2026-10-02 | **Last Updated**: 2026-10-06
3
+ **Created**: 2026-10-02 | **Last Updated**: 2026-10-07
4
4
 
5
5
  Renderers, logos and photo placeholders for the Otto flight, hotel, room and car cards and the seat map. Each `render_*` MCP tool returns card data and a snippet. The host's model mounts the card in its own widget sandbox, which loads files only from a few public CDNs, so the assets are served from jsDelivr.
6
6
 
7
7
  ## Contents
8
8
 
9
9
  - `flight-card.js`, `hotel-card.js`, `room-card.js` and `car-card.js` each draw one card and expose `OttoFlightCard`, `OttoHotelCard`, `OttoRoomCard` or `OttoCarCard` with `render(rootElement, card)`. Each carries its own CSS and only the icons it uses inline, so the sandbox loads one script per card. The server's snippet loads jsDelivr's minified copy (`*-card.min.js`). Since 0.10.0, `room-card.js` draws only a row of room buttons, and since 0.11.0 `car-card.js` draws only a row of car buttons: the host draws the rooms or cars, with photos, from the same tool's MCP Apps resource.
10
- - `seat-map.js` exposes `OttoSeatMap.render(rootElement, card)` (since 0.12.0). It draws one flight's cabin from `render_elastic_seat_map`. The server lays out the columns, aisles and rows for the aircraft and cabin, so the script only draws them. Clicking a seat marks it New selection without sending anything; since 0.13.0 the **Update seat to 15D** button then sends "Update seat to 15D (route)".
10
+ - `seat-map.js` exposes `OttoSeatMap.render(rootElement, card)` (since 0.12.0). It draws one flight's cabin from `render_elastic_seat_map`. The server lays out the columns, aisles and rows for the aircraft and cabin, so the script only draws them. Clicking a seat marks it New selection without sending anything; since 0.13.0 the **Update seat to 15D** button then sends "Update seat to 15D (route)". Since 0.14.0 it tags exit rows and bulkhead rows, from the card's `exit` and `bulkhead` row flags. Also since 0.14.0, each row of the flight card's fare list starts its facts line with the fare's refund wording. Since 0.15.0 the seat map draws each lavatory in `card.lavs` as one labeled block over its seat columns.
11
11
  - Select (Show rooms on the hotel card) sends the pick as the traveler's message through the host's `sendPrompt()`: "Book flight …", "Show rooms at …", "Select room …" or "Select car …". The message carries no id.
12
12
  - `*-card.css` holds each card's CSS as a separate file, for reference.
13
13
  - `marks/<IATA>.png` holds airline logos, and `marks/cars/<code>.svg` holds car-company logos.
package/flight-card.js CHANGED
@@ -185,7 +185,10 @@
185
185
  const row = el("div", "ladderRow" + (fare.recommended ? " recommended" : ""));
186
186
  const label = el("div", "ladderLabel");
187
187
  label.append(el("div", "ladderName", fare.name + (fare.recommended ? " · Recommended" : "")));
188
- const facts = [...(fare.facts || []), fare.within_policy === false ? "Out of policy" : ""].filter(Boolean).join(" · ");
188
+ // Refund wording leads the facts line; it comes from the fare, never inferred.
189
+ const facts = [oneLine(fare.refundable), ...(fare.facts || []), fare.within_policy === false ? "Out of policy" : ""]
190
+ .filter(Boolean)
191
+ .join(" · ");
189
192
  if (facts) label.append(el("div", "ladderFacts", facts));
190
193
  const right = el("div", "ladderRight");
191
194
  right.append(el("div", "ladderAmount", money(fare.price)), selectButton("btn", fare));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottotheagent/mcp-card-assets",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Renderers, styles, logos and photo placeholders for the Otto flight, hotel, room and car cards and the seat map that Claude draws in its widget sandbox.",
5
5
  "license": "UNLICENSED",
6
6
  "files": [
package/seat-map.css CHANGED
@@ -1 +1 @@
1
- :root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme="dark"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role="button"]{cursor:pointer;user-select:none;}.smSeat[role="button"]:hover{border-color:var(--otto-accent);}.smSeat[role="button"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}
1
+ :root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme="dark"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smLav{box-sizing:border-box;flex-shrink:0;height:var(--sm-seat);display:grid;place-items:center;overflow:hidden;border:1px dashed var(--otto-card-border);border-radius:6px;background:var(--otto-strip-bg);color:var(--otto-text-secondary);font-size:var(--otto-text-sm);white-space:nowrap;}.smRow.bulkhead{padding-top:var(--otto-space-2);border-top:3px solid var(--otto-text-secondary);}.smTag{flex-shrink:0;margin-left:var(--otto-space-2);font-size:var(--otto-text-sm);color:var(--otto-text-secondary);white-space:nowrap;}.smTag.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role="button"]{cursor:pointer;user-select:none;}.smSeat[role="button"]:hover{border-color:var(--otto-accent);}.smSeat[role="button"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}
package/seat-map.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * spec.bar draws the options as one wrapping row of buttons with no title, for a card
12
12
  * whose details the host already shows above it.
13
13
  */
14
- const CSS = ":root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme=\"dark\"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role=\"button\"]{cursor:pointer;user-select:none;}.smSeat[role=\"button\"]:hover{border-color:var(--otto-accent);}.smSeat[role=\"button\"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}";
14
+ const CSS = ":root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme=\"dark\"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smLav{box-sizing:border-box;flex-shrink:0;height:var(--sm-seat);display:grid;place-items:center;overflow:hidden;border:1px dashed var(--otto-card-border);border-radius:6px;background:var(--otto-strip-bg);color:var(--otto-text-secondary);font-size:var(--otto-text-sm);white-space:nowrap;}.smRow.bulkhead{padding-top:var(--otto-space-2);border-top:3px solid var(--otto-text-secondary);}.smTag{flex-shrink:0;margin-left:var(--otto-space-2);font-size:var(--otto-text-sm);color:var(--otto-text-secondary);white-space:nowrap;}.smTag.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role=\"button\"]{cursor:pointer;user-select:none;}.smSeat[role=\"button\"]:hover{border-color:var(--otto-accent);}.smSeat[role=\"button\"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}";
15
15
  const ICONS = {"hotels/check-regular-full":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M534 132.5C544.8 140.2 547.2 155.2 539.5 166L275.5 534C271.4 539.7 265 543.4 258 543.9C251 544.4 244 542 239 537L103 401C93.6 391.6 93.6 376.4 103 367.1C112.4 357.8 127.6 357.7 136.9 367.1L253 483L500.5 138C508.2 127.2 523.2 124.8 534 132.5z\"/></svg>","hotels/circle-info-regular-full":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M320 112C434.9 112 528 205.1 528 320C528 434.9 434.9 528 320 528C205.1 528 112 434.9 112 320C112 205.1 205.1 112 320 112zM320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM280 400C266.7 400 256 410.7 256 424C256 437.3 266.7 448 280 448L360 448C373.3 448 384 437.3 384 424C384 410.7 373.3 400 360 400L352 400L352 312C352 298.7 341.3 288 328 288L280 288C266.7 288 256 298.7 256 312C256 325.3 266.7 336 280 336L304 336L304 400L280 400zM320 256C337.7 256 352 241.7 352 224C352 206.3 337.7 192 320 192C302.3 192 288 206.3 288 224C288 241.7 302.3 256 320 256z\"/></svg>"};
16
16
  const SYMBOLS = { USD: "$", EUR: "€", GBP: "£", JPY: "¥", CNY: "¥", AUD: "A$", CAD: "C$" };
17
17
  // The widget cannot see when the host's reply ends, so the lock lifts on a timer.
@@ -174,7 +174,8 @@ function mount(root, card, spec) {
174
174
  * and cabin draws the same way. Collapsed, it shows the traveler's row and one row either
175
175
  * side; Expand shows the whole cabin. Clicking another seat only marks it New selection and
176
176
  * shows "Update seat to 15D"; that button sends "Update seat to 15D (SEA → JFK)" as the
177
- * traveler's message. */
177
+ * traveler's message. A lavatory (card.lavs) draws as one labeled block over its seat columns;
178
+ * a lavatory row without seats becomes a row of its own. */
178
179
 
179
180
  function render(root, card) {
180
181
  injectCss("otto-seat-map-css");
@@ -184,8 +185,15 @@ function render(root, card) {
184
185
  return;
185
186
  }
186
187
 
188
+ // Row number → lavatory seat letters. Seatless lavatory rows join the rows in number order.
189
+ const lavs = new Map((card.lavs || []).map((lav) => [lav.row, lav.cols]));
190
+ const rows = [...card.rows];
191
+ for (const number of lavs.keys()) {
192
+ if (!rows.some((row) => row.number === number)) rows.push({ number, seats: card.columns.map(() => null), lavOnly: true });
193
+ }
194
+ rows.sort((a, b) => a.number - b.number);
187
195
  const seats = new Map();
188
- card.rows.forEach((row, index) => row.seats.forEach((seat) => seat && seats.set(seat.id, { seat, index })));
196
+ rows.forEach((row, index) => row.seats.forEach((seat) => seat && seats.set(seat.id, { seat, index })));
189
197
  const home = seats.get(card.current_seat)?.index ?? 0;
190
198
  const aisles = new Set(card.aisles_after || []);
191
199
  let expanded = false;
@@ -203,7 +211,10 @@ function render(root, card) {
203
211
  return { free: "free", paid: "paid", unavailable: "off" }[seat.state] || "off";
204
212
  }
205
213
 
206
- function seatNode(seat) {
214
+ // Row facts come only from the card (the seats table's is_exit_row / is_bulkhead).
215
+ const rowFacts = (row) => [row.exit && "Exit row", row.bulkhead && "Bulkhead"].filter(Boolean);
216
+
217
+ function seatNode(seat, facts) {
207
218
  const cls = seatClass(seat);
208
219
  const legroom = seat.extra_legroom && cls !== "off" ? " legroom" : "";
209
220
  if (cls === "off") {
@@ -219,8 +230,9 @@ function render(root, card) {
219
230
  button.setAttribute("role", "button");
220
231
  button.tabIndex = busy ? -1 : 0;
221
232
  button.dataset.seat = seat.id;
222
- button.title =
223
- describe(seat) + (seat.extra_legroom ? " · extra legroom" : "") + (cls === "pick" ? " · new selection" : "");
233
+ button.title = [describe(seat), seat.extra_legroom && "extra legroom", ...facts.map((f) => f.toLowerCase())]
234
+ .filter(Boolean)
235
+ .join(" · ") + (cls === "pick" ? " · new selection" : "");
224
236
  button.setAttribute("aria-label", button.title);
225
237
  button.setAttribute("aria-pressed", String(cls === "mine" || cls === "pick"));
226
238
  return button;
@@ -236,15 +248,41 @@ function render(root, card) {
236
248
  return line;
237
249
  }
238
250
 
251
+ // One block over a run of empty lavatory cells, as wide as the seats and aisles it replaces.
252
+ function lavBlock(count, aisleCount) {
253
+ const block = el("span", "smLav", count >= 3 ? "Lavatory" : "WC");
254
+ block.style.width = `calc(var(--sm-seat) * ${count} + var(--sm-aisle) * ${aisleCount} + var(--sm-gap) * ${count + aisleCount - 1})`;
255
+ block.title = "Lavatory";
256
+ block.setAttribute("role", "img");
257
+ block.setAttribute("aria-label", "Lavatory");
258
+ return block;
259
+ }
260
+
261
+ // An exit row or bulkhead row gets a tag after its last seat; a bulkhead also draws the wall above it.
239
262
  function seatRow(row) {
240
- const line = el("div", "smRow");
241
- const number = el("span", "smNum" + (row.exit ? " exit" : ""), row.number);
242
- if (row.exit) number.title = "Exit row";
263
+ const facts = rowFacts(row);
264
+ const lav = lavs.get(row.number) || "";
265
+ const isLav = (i) => !row.seats[i] && lav.includes(card.columns[i]);
266
+ const line = el("div", "smRow" + (row.bulkhead ? " bulkhead" : ""));
267
+ const number = el("span", "smNum" + (row.exit ? " exit" : ""), row.lavOnly ? "" : row.number);
268
+ if (facts.length) number.title = facts.join(" · ");
243
269
  line.append(number);
244
270
  row.seats.forEach((seat, i) => {
245
- line.append(seat ? seatNode(seat) : el("span", "smEmpty"));
271
+ if (isLav(i)) {
272
+ if (i > 0 && isLav(i - 1)) return;
273
+ let end = i;
274
+ while (end + 1 < row.seats.length && isLav(end + 1)) end += 1;
275
+ let inner = 0;
276
+ for (let k = i; k < end; k += 1) if (aisles.has(k)) inner += 1;
277
+ line.append(lavBlock(end - i + 1, inner));
278
+ if (aisles.has(end)) line.append(el("span", "smAisle"));
279
+ return;
280
+ }
281
+ line.append(seat ? seatNode(seat, facts) : el("span", "smEmpty"));
246
282
  if (aisles.has(i)) line.append(el("span", "smAisle"));
247
283
  });
284
+ if (row.exit) line.append(el("span", "smTag exit", "Exit"));
285
+ if (row.bulkhead) line.append(el("span", "smTag", "Bulkhead"));
248
286
  return line;
249
287
  }
250
288
 
@@ -283,7 +321,7 @@ function render(root, card) {
283
321
  const text = el("div");
284
322
  text.append(el("div", "title", card.title), el("div", "subtitle", card.subtitle));
285
323
  head.append(text);
286
- if (card.rows.length > 3) {
324
+ if (rows.length > 3) {
287
325
  const toggle = el("button", "btn smToggle", expanded ? "Collapse" : "Expand");
288
326
  toggle.type = "button";
289
327
  toggle.dataset.toggle = "1";
@@ -291,9 +329,9 @@ function render(root, card) {
291
329
  head.append(toggle);
292
330
  }
293
331
 
294
- const rows = expanded ? card.rows : card.rows.slice(Math.max(0, home - 1), home + 2);
332
+ const visible = expanded ? rows : rows.slice(Math.max(0, home - 1), home + 2);
295
333
  const grid = el("div", "smGrid");
296
- grid.append(lettersRow(), ...rows.map(seatRow));
334
+ grid.append(lettersRow(), ...visible.map(seatRow));
297
335
  const scroller = el("div", "smScroll");
298
336
  scroller.append(grid);
299
337