@ottotheagent/mcp-card-assets 0.10.0 → 0.12.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,18 +1,19 @@
1
1
  # @ottotheagent/mcp-card-assets
2
2
 
3
- **Created**: 2026-10-02 | **Last Updated**: 2026-10-05
3
+ **Created**: 2026-10-02 | **Last Updated**: 2026-10-06
4
4
 
5
- Renderers, logos and photo placeholders for the Otto flight, hotel, room and car cards. 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.
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
- - `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: the host draws the rooms, with photos, from the same tool's MCP Apps resource.
10
- - 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 "Book car …". The message carries no id.
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. Update Seat sends "Update seat to … (route)".
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.
11
12
  - `*-card.css` holds each card's CSS as a separate file, for reference.
12
13
  - `marks/<IATA>.png` holds airline logos, and `marks/cars/<code>.svg` holds car-company logos.
13
- - `placeholders/car.svg` fills the car card's photo plate, because host sandboxes block supplier photos. The hotel card has no photo plate: the host's own map shows the hotels with their photos. `hotel.svg` and `room.svg` stay for renderers before 0.8.0 and 0.10.0.
14
+ - `placeholders/` holds photo placeholders for renderers before 0.8.0 (`hotel.svg`), 0.10.0 (`room.svg`) and 0.11.0 (`car.svg`). Current renderers draw no photo plate: host sandboxes block supplier photos, so the host's own map or MCP Apps carousel shows them.
14
15
 
15
- The sources are `src/flight-card.js`, and `src/core.js` plus `src/<kind>-card.js` for the hotel, room and car cards. `scripts/mcp/build_card_assets.py` writes the published files from them, from the card CSS in `server/mcp/apps/static/`, and from the logo tables. The generated files are not committed; `placeholders/` is.
16
+ The sources are `src/flight-card.js`, and `src/core.js` plus `src/<kind>-card.js` (or `src/seat-map.js`) for the other renderers. `scripts/mcp/build_card_assets.py` writes the published files from them, from the card CSS in `server/mcp/apps/static/`, and from the logo tables. The generated files are not committed; `placeholders/` is.
16
17
 
17
18
  ## Publish a new version
18
19
 
package/car-card.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);}}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}.btn.chosen:disabled{opacity:1;}#root{min-height:140px;}.opt{padding:0 0 var(--otto-space-7);overflow:hidden;}.opt>:not(.photo){margin-inline:var(--otto-space-7);}.photo{position:relative;height:var(--otto-photo-height);overflow:hidden;background:#ffffff;}.photo img:not(.vendor){width:100%;height:100%;object-fit:contain;display:block;}.chips{position:absolute;top:var(--otto-space-4);left:var(--otto-space-4);right:132px;display:flex;flex-wrap:wrap;gap:var(--otto-space-1);}.vendor{position:absolute;top:var(--otto-space-4);right:var(--otto-space-4);height:var(--otto-mark-size);max-width:110px;padding:2px var(--otto-space-2);box-sizing:border-box;border-radius:4px;background:#ffffff;object-fit:contain;}.vendorName{position:absolute;top:var(--otto-space-4);right:var(--otto-space-4);max-width:110px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;line-height:var(--otto-mark-size);font-size:var(--otto-text-md);font-weight:var(--otto-weight-semibold);color:#0a0a0a;}.name{margin-top:var(--otto-space-6);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);line-height:1.3;}.loyalty{display:flex;align-items:center;gap:var(--otto-space-2);margin-top:var(--otto-space-1);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.features{display:flex;flex-wrap:wrap;gap:var(--otto-space-1) var(--otto-space-5);margin-top:var(--otto-space-5);}.fact{display:flex;align-items:center;gap:var(--otto-space-2);font-size:var(--otto-text-base);color:var(--otto-text-body);}.fact svg{flex-shrink:0;}.fare{display:flex;align-items:center;gap:var(--otto-space-5);margin-top:var(--otto-space-7);}.fare .btn{flex:0 0 45%;}.fare>div{min-width:0;}.amount{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.per,.total{font-size:var(--otto-text-md);font-weight:var(--otto-weight-regular);color:var(--otto-text-secondary);}.per{color:inherit;}.total{display:block;}.btn.chosen{background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}
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);}}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}.btn.chosen:disabled{opacity:1;}#root{min-height:0;}.bar{display:flex;flex-wrap:wrap;gap:var(--otto-space-3);}.bar .btn{max-width:100%;}.bar .label{overflow:hidden;text-overflow:ellipsis;}.btn.chosen{background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}
package/car-card.js CHANGED
@@ -11,8 +11,8 @@
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);}}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}.btn.chosen:disabled{opacity:1;}#root{min-height:140px;}.opt{padding:0 0 var(--otto-space-7);overflow:hidden;}.opt>:not(.photo){margin-inline:var(--otto-space-7);}.photo{position:relative;height:var(--otto-photo-height);overflow:hidden;background:#ffffff;}.photo img:not(.vendor){width:100%;height:100%;object-fit:contain;display:block;}.chips{position:absolute;top:var(--otto-space-4);left:var(--otto-space-4);right:132px;display:flex;flex-wrap:wrap;gap:var(--otto-space-1);}.vendor{position:absolute;top:var(--otto-space-4);right:var(--otto-space-4);height:var(--otto-mark-size);max-width:110px;padding:2px var(--otto-space-2);box-sizing:border-box;border-radius:4px;background:#ffffff;object-fit:contain;}.vendorName{position:absolute;top:var(--otto-space-4);right:var(--otto-space-4);max-width:110px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;line-height:var(--otto-mark-size);font-size:var(--otto-text-md);font-weight:var(--otto-weight-semibold);color:#0a0a0a;}.name{margin-top:var(--otto-space-6);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);line-height:1.3;}.loyalty{display:flex;align-items:center;gap:var(--otto-space-2);margin-top:var(--otto-space-1);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.features{display:flex;flex-wrap:wrap;gap:var(--otto-space-1) var(--otto-space-5);margin-top:var(--otto-space-5);}.fact{display:flex;align-items:center;gap:var(--otto-space-2);font-size:var(--otto-text-base);color:var(--otto-text-body);}.fact svg{flex-shrink:0;}.fare{display:flex;align-items:center;gap:var(--otto-space-5);margin-top:var(--otto-space-7);}.fare .btn{flex:0 0 45%;}.fare>div{min-width:0;}.amount{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.per,.total{font-size:var(--otto-text-md);font-weight:var(--otto-weight-regular);color:var(--otto-text-secondary);}.per{color:inherit;}.total{display:block;}.btn.chosen{background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}";
15
- const ICONS = {"cars/person-seat-reclined-solid-full":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M312 136C312 105.1 286.9 80 256 80C225.1 80 200 105.1 200 136C200 166.9 225.1 192 256 192C286.9 192 312 166.9 312 136zM354.2 384L330.5 265.5C325.7 241.4 304.5 224 279.9 224C247.3 224 222.9 253.8 229.3 285.7L251.5 396.5C257.5 426.4 283.7 447.9 314.3 447.9L327.4 447.9C327.8 447.9 328.2 447.9 328.7 447.9L414.8 447.9C421.5 447.9 427.4 452 429.8 458.3L466 555.2C472 571.3 489.8 579.8 506.1 574.3L554.1 558.3C570.9 552.7 579.9 534.6 574.3 517.8C568.7 501 550.6 492 533.8 497.6L515.1 503.8L489.6 435.8C477.9 404.6 448 383.9 414.7 383.9L354.2 383.9zM159.2 248.7C155.2 231.5 137.9 220.8 120.7 224.9C103.5 229 92.8 246.2 96.9 263.4L143.5 461.4C157 519.1 208.6 560 268 560L352 560C369.7 560 384 545.7 384 528C384 510.3 369.7 496 352 496L268 496C238.3 496 212.5 475.6 205.7 446.7L159.1 248.7z\"/></svg>","cars/suitcase-regular-full":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M264 112L376 112C380.4 112 384 115.6 384 120L384 160L256 160L256 120C256 115.6 259.6 112 264 112zM208 120L208 160L128 160C92.7 160 64 188.7 64 224L64 480C64 515.3 92.7 544 128 544L512 544C547.3 544 576 515.3 576 480L576 224C576 188.7 547.3 160 512 160L432 160L432 120C432 89.1 406.9 64 376 64L264 64C233.1 64 208 89.1 208 120zM232 208L416 208L416 496L224 496L224 208L232 208zM176 208L176 496L128 496C119.2 496 112 488.8 112 480L112 224C112 215.2 119.2 208 128 208L176 208zM464 496L464 208L512 208C520.8 208 528 215.2 528 224L528 480C528 488.8 520.8 496 512 496L464 496z\"/></svg>","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>","hotels/coin-light-full":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M192 396.7C225.3 407.5 263.4 414.3 304 415.7L304 479.7C262.3 478.3 224.1 471.6 192 461.4L192 396.7zM96 341.7C112.2 358.2 134.1 372.6 160 384.3L160 449C157.7 447.9 155.5 446.9 153.3 445.8C110.4 424.3 96 400.6 96 383.9L96 341.7zM320 128C178.6 128 64 192.5 64 272L64 384C64 454.7 178.6 512 320 512C461.4 512 576 454.7 576 384L576 272C576 192.5 461.4 128 320 128zM480 384.4C505.9 372.7 527.8 358.2 544 341.8L544 384C544 400.7 529.6 424.4 486.7 445.9C484.5 447 482.3 448.1 480 449.1L480 384.4zM336 415.7C376.6 414.3 414.7 407.5 448 396.7L448 461.4C415.9 471.7 377.7 478.3 336 479.7L336 415.7zM96 272C96 210.1 196.3 160 320 160C443.7 160 544 210.1 544 272C544 333.9 443.7 384 320 384C196.3 384 96 333.9 96 272zM320 352C417.2 352 496 316.2 496 272C496 227.8 417.2 192 320 192C222.8 192 144 227.8 144 272C144 316.2 222.8 352 320 352zM184 272C184 245.5 244.9 224 320 224C395.1 224 456 245.5 456 272C456 298.5 395.1 320 320 320C244.9 320 184 298.5 184 272z\"/></svg>"};
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);}}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}.btn.chosen:disabled{opacity:1;}#root{min-height:0;}.bar{display:flex;flex-wrap:wrap;gap:var(--otto-space-3);}.bar .btn{max-width:100%;}.bar .label{overflow:hidden;text-overflow:ellipsis;}.btn.chosen{background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}";
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.
18
18
  const BUSY_MS = 60000;
@@ -169,183 +169,53 @@ function mount(root, card, spec) {
169
169
  return handle;
170
170
  }
171
171
 
172
- /* Otto car card: OttoCarCard.render(root, card), card from render_cars.
173
- * Select sends "Book car …" as the traveler's message. */
174
-
175
- const OUT_OF_POLICY = "Out of policy";
176
- const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
177
- const SVG_NS = "http://www.w3.org/2000/svg";
178
-
179
- // "2026-10-03T12:00:00" (pickup-local) → "Oct 3". Read off the string, not through Date,
180
- // which would shift it into the viewer's time zone.
181
- function shortDate(iso) {
182
- const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso || "");
183
- return match ? `${MONTHS[Number(match[2]) - 1]} ${Number(match[3])}` : "";
184
- }
185
-
186
- // Facts about the shown set, not a ranking: every option tied for the best value is a
187
- // candidate, and none is when all are tied. Totals are compared only in the card's currency.
188
- function carCallouts(items) {
189
- const byId = new Map(items.map((item) => [item.id, []]));
190
- const award = (label, value, pick) => {
191
- const values = items.map(value).filter((v) => typeof v === "number");
192
- if (values.length < 2) return;
193
- const best = pick(...values);
194
- if (values.every((v) => v === best)) return;
195
- for (const item of items) if (value(item) === best) byId.get(item.id).push(label);
196
- };
197
- award("Lowest total", (item) => (item.currency ? null : item.total_rate), Math.min);
198
- award("Most bags", (item) => item.bags, Math.max);
199
- return byId;
200
- }
201
-
202
- // One chip per card, each chip text on one card only, in card order; the policy
203
- // warning outranks everything and is exempt from the one-card rule.
204
- function assignChips(items, labels) {
205
- const taken = new Set();
206
- const byId = new Map();
207
- for (const item of items) {
208
- const candidates = [];
209
- if (item.within_policy === false) candidates.push([OUT_OF_POLICY, "chip caution"]);
210
- if (item.company_preference_label) candidates.push([item.company_preference_label, "chip"]);
211
- if (item.is_corporate_rate) candidates.push(["Corporate rate", "chip"]);
212
- for (const label of labels.get(item.id) || []) candidates.push([label, "chip"]);
213
- const chip = candidates.find(([text]) => !taken.has(text));
214
- if (chip && chip[0] !== OUT_OF_POLICY) taken.add(chip[0]);
215
- byId.set(item.id, chip || null);
216
- }
217
- return byId;
218
- }
219
-
220
- function svg(tag, attrs) {
221
- const node = document.createElementNS(SVG_NS, tag);
222
- for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
223
- return node;
224
- }
225
-
226
- // Drawn line icons in the text colour, for a name the icon table lacks.
227
- const STROKE = { fill: "none", stroke: "currentColor", "stroke-width": "1.3", "stroke-linecap": "round" };
228
- const LINE_ICONS = {
229
- seats: [
230
- ["circle", { cx: "6", cy: "5", r: "2.3" }],
231
- ["path", { d: "M1.8 13.5c0-2.4 1.9-4 4.2-4s4.2 1.6 4.2 4" }],
232
- ["circle", { cx: "11.4", cy: "5.6", r: "1.8" }],
233
- ["path", { d: "M11.2 9.4c1.8.1 3 1.5 3 3.6" }],
234
- ],
235
- bags: [
236
- ["rect", { x: "3.5", y: "4.5", width: "9", height: "9.5", rx: "1.5" }],
237
- ["path", { d: "M6 4.5V2.5h4v2M6.2 7.5v4M9.8 7.5v4" }],
238
- ],
239
- };
240
- const TABLE_ICONS = { seats: "cars/person-seat-reclined-solid-full", bags: "cars/suitcase-regular-full" };
241
-
242
- function lineIcon(name) {
243
- if (ICONS[TABLE_ICONS[name]]) return icon(TABLE_ICONS[name]);
244
- const node = svg("svg", { width: "15", height: "15", viewBox: "0 0 16 16", "aria-hidden": "true" });
245
- for (const [tag, attrs] of LINE_ICONS[name]) node.append(svg(tag, { ...STROKE, ...attrs }));
246
- return node;
247
- }
172
+ /* Otto car buttons: OttoCarCard.render(root, card), card from render_elastic_cars.
173
+ * One button per car, labeled with its company and class; a click sends "Select car …" as
174
+ * the traveler's message. The host draws the cars themselves, with photos, from the same
175
+ * tool result (see the car_rendering_elastic skill). */
248
176
 
249
177
  function render(root, card) {
250
178
  injectCss("otto-car-card-css");
251
- const money = (value, item) => formatMoney(value, item?.currency || card.currency);
252
179
  const exactMoney = (value, item) => formatExactMoney(value, item?.currency || card.currency);
253
- const chips = assignChips(card.items || [], carCallouts(card.items || []));
254
-
255
- // Vendor and class as a traveler would say them; the card number only when another
256
- // option reads the same.
257
- function choiceText(item) {
258
- const name = oneLine(`${item.vendor} ${item.class_label}`);
259
- const key = (other) => oneLine(`${other.vendor} ${other.class_label}`).toLowerCase();
260
- const twin = card.items.some((other) => other !== item && key(other) === key(item));
261
- const total = exactMoney(item.total_rate, item);
262
- return [twin ? `${name} (No. ${card.items.indexOf(item) + 1} on the card)` : name, total ? `${total} total` : ""]
263
- .filter(Boolean)
264
- .join(", ");
265
- }
180
+ const name = (item) => oneLine(`${item.vendor} ${item.class_label}`);
181
+ const total = (item) => {
182
+ const amount = exactMoney(item.total_rate, item);
183
+ return amount ? `${amount} total` : "";
184
+ };
266
185
 
267
- function vendor(item) {
268
- if (!item.vendor_mark_url) return el("div", "vendorName", item.vendor);
269
- const img = el("img", "vendor");
270
- img.src = item.vendor_mark_url;
271
- img.alt = item.vendor;
272
- img.title = item.vendor;
273
- img.addEventListener("error", () => img.replaceWith(el("div", "vendorName", item.vendor)));
274
- return img;
186
+ // The pick as a traveler would say it: company, class and total, plus the card
187
+ // position when another car reads the same, so the model can tell them apart.
188
+ function describe(item) {
189
+ const text = (other) => [name(other), total(other)].filter(Boolean).join(", ");
190
+ const twin = card.items.some((other) => other !== item && text(other).toLowerCase() === text(item).toLowerCase());
191
+ return twin ? `${text(item)} (No. ${card.items.indexOf(item) + 1} on the card)` : text(item);
275
192
  }
276
193
 
277
- function fact(name, text) {
278
- const line = el("div", "fact");
279
- line.append(lineIcon(name), el("span", null, text));
280
- return line;
281
- }
282
-
283
- // The rental terms after the caller's reasons: ticked lines, then caveats under the info mark.
284
- function terms(item) {
285
- const ticked = [];
286
- const noted = [];
287
- if (item.refundable === true) {
288
- const until = shortDate(item.free_cancellation_until);
289
- ticked.push(until ? `Free cancellation until ${until}` : "Free cancellation");
290
- } else if (item.refundable === false) {
291
- noted.push("Non-refundable");
292
- }
293
- // search_cars returns pay-at-pickup rates only; nothing is charged at booking.
294
- ticked.push("Pay at pickup");
295
- if (item.concern) noted.push(item.concern);
296
- return [ticked, noted];
194
+ // Company and class, plus the first of total and position that no same-named car
195
+ // shares: a vendor can list one class at several rates.
196
+ function label(item) {
197
+ const key = (other) => name(other).toLowerCase();
198
+ const twins = card.items.filter((other) => other !== item && key(other) === key(item));
199
+ if (!twins.length) return name(item);
200
+ const details = (other) => [total(other), `No. ${card.items.indexOf(other) + 1}`];
201
+ const detail = details(item).find((value, i) => value && twins.every((twin) => details(twin)[i] !== value));
202
+ return detail ? `${name(item)} · ${detail}` : name(item);
297
203
  }
298
204
 
299
205
  function option(item, ui) {
300
- const box = el("div", "opt" + (ui.chosen ? " chosen" : ""));
301
- const photo = el("div", "photo");
302
- photo.append(placeholder("car"));
303
- const chip = chips.get(item.id);
304
- if (chip) {
305
- const row = el("div", "chips");
306
- row.append(el("span", chip[1], chip[0]));
307
- photo.append(row);
308
- }
309
- photo.append(vendor(item));
310
- box.append(photo, el("div", "name", item.class_label));
311
-
312
- // The vendor's program, not a promise of points: earning depends on the rental.
313
- if (item.loyalty_program) {
314
- const loyalty = el("div", "loyalty");
315
- loyalty.append(icon("hotels/coin-light-full", "◎"), el("span", null, `Eligible for ${item.loyalty_program}`));
316
- box.append(loyalty);
317
- }
318
- const features = el("div", "features");
319
- if (item.seats) features.append(fact("seats", item.seats === 1 ? "1 seat" : `${item.seats} seats`));
320
- if (item.bags) features.append(fact("bags", item.bags === 1 ? "1 bag" : `${item.bags} bags`));
321
- if (features.childElementCount) box.append(features);
322
-
323
- const fare = el("div", "fare");
324
- const price = el("div");
325
- // The per-day rate is the pre-tax base; the total is what the counter charges.
326
- if (item.daily_rate !== null && item.daily_rate !== undefined) {
327
- const amount = el("div", "amount", money(item.daily_rate, item));
328
- amount.append(el("span", "per", "/day"));
329
- price.append(amount);
330
- if (item.total_rate !== null && item.total_rate !== undefined) {
331
- price.append(el("span", "total", `${money(item.total_rate, item)} total`));
332
- }
333
- } else if (item.total_rate !== null && item.total_rate !== undefined) {
334
- const amount = el("div", "amount", money(item.total_rate, item));
335
- amount.append(el("span", "per", " total"));
336
- price.append(amount);
337
- }
338
- fare.append(ui.pick("Select"), price);
339
- box.append(fare);
340
- const [ticked, noted] = terms(item);
341
- box.append(renderInsights([...(item.reasons || []), ...ticked], noted));
342
- return box;
206
+ const button = el("button", "btn" + (ui.chosen ? " chosen" : ""));
207
+ button.dataset.select = item.id;
208
+ button.title = label(item);
209
+ if (ui.chosen) button.append(icon("hotels/check-regular-full", "✓"));
210
+ button.append(el("span", "label", label(item)));
211
+ return button;
343
212
  }
344
213
 
345
214
  mount(root, card, {
346
215
  empty: "No rental cars to show.",
216
+ bar: true,
347
217
  option,
348
- message: (item) => `Book car ${choiceText(item)}`,
218
+ message: (item) => `Select car ${describe(item)}`,
349
219
  });
350
220
  }
351
221
 
package/flight-card.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;}.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);}}.carrier{display:flex;align-items:center;gap:var(--otto-space-3);min-height:var(--otto-chip-height);}.airline{flex:1;min-width:0;display:flex;align-items:center;gap:var(--otto-space-4);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);}.airlineName{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.chips{display:flex;gap:var(--otto-space-1);flex-shrink:0;}.mark{width:var(--otto-mark-size);height:var(--otto-mark-size);border-radius:50%;flex-shrink:0;display:grid;place-items:center;font-size:var(--otto-text-xs);font-weight:var(--otto-weight-semibold);letter-spacing:0.3px;color:#fff;background:#5b6470;}img.mark{object-fit:contain;background:transparent;border:0;padding:0;}.fltline{display:flex;justify-content:space-between;margin-top:var(--otto-space-8);font-size:var(--otto-text-md);color:var(--otto-text-body);}.times,.ports{display:flex;align-items:center;justify-content:space-between;}.times{margin-top:var(--otto-space-2);gap:var(--otto-space-3);}.ports{margin-top:var(--otto-space-1);font-size:var(--otto-text-lg);color:var(--otto-text-body);}.clock{font-size:var(--otto-text-xl);font-weight:var(--otto-weight-semibold);white-space:nowrap;}.plus{font-size:var(--otto-text-sm);vertical-align:super;color:var(--otto-caution-text);}.rule{flex:1;height:1px;background:var(--otto-divider);}.plane{color:var(--otto-text-secondary);}.fareBlock{margin-top:var(--otto-space-6);display:flex;flex-direction:column;}.strip{height:66px;box-sizing:border-box;display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-3);padding:9px var(--otto-space-5) 0;border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);font-size:var(--otto-text-base);}.strip + .fareRow{margin-top:-27px;}.lowest{min-width:0;font-weight:var(--otto-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.lowestName{font-weight:var(--otto-weight-regular);color:var(--otto-text-secondary);}.strip button.link{flex-shrink:0;display:inline-flex;gap:6px;height:auto;min-height:0;margin:0;padding:0;border:0;border-radius:0;box-shadow:none;background:none;appearance:none;cursor:pointer;align-items:center;font-size:var(--otto-text-base);line-height:var(--otto-line);color:var(--otto-text-primary);}@media (hover:hover){.opt.hasFares:hover .link{color:var(--otto-link-hover);}.opt.hasFares:hover .linkText{text-decoration:underline;}}.fareRow{position:relative;height:78px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-2) var(--otto-space-5) var(--otto-space-2) var(--otto-space-6);border-radius:var(--otto-radius-fare);background:var(--otto-fare-bg);color:var(--otto-on-fare);}.fareText{min-width:0;}.amount{font-size:var(--otto-text-2xl);font-weight:var(--otto-weight-semibold);}.fareName{font-size:var(--otto-text-base);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.fareFacts{font-size:var(--otto-text-md);opacity:0.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.tag{font-size:var(--otto-text-md);}.fareRow .btn.onFare{padding:9px 20px;background:var(--otto-on-fare);color:var(--otto-fare-bg);border-color:var(--otto-on-fare);}.fareRow .btn.onFare.chosen{padding:9px var(--otto-space-7);border-color:var(--otto-accent);}.opt.hasFares{cursor:pointer;}.opt.open{border-color:var(--otto-accent);}.fareList{margin-top:var(--otto-space-7);padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);display:flex;flex-direction:column;gap:var(--otto-space-3);}.fareListHead{margin-bottom:var(--otto-space-1);font-size:var(--otto-text-md);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderRow{display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-3) var(--otto-space-3) var(--otto-space-3) var(--otto-space-6);border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);}.ladderRow.recommended{border-color:var(--otto-text-primary);}@media (hover:hover){.ladderRow:hover{border-color:var(--otto-accent);}}.ladderLabel{min-width:0;}.ladderName{font-size:var(--otto-text-base);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderFacts{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.ladderRight{flex-shrink:0;display:flex;align-items:center;gap:var(--otto-space-5);}.ladderAmount{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.ladderRow .btn{min-height:36px;padding:6px var(--otto-space-6);background:var(--otto-card-bg);border-color:var(--otto-text-primary);}.btn.chosen:not(.onFare){background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}#root.busy .opt.open:not(.chosen){border-color:var(--otto-accent);}#root.busy .opt.hasFares:hover .link{color:var(--otto-text-primary);}#root.busy .opt.hasFares:hover .linkText{text-decoration:none;}#root.busy .ladderRow:hover{border-color:var(--otto-divider);}#root.busy .ladderRow.recommended:hover{border-color:var(--otto-text-primary);}.strip button.link:disabled{cursor:default;opacity:0.45;}.btn.chosen:disabled{opacity: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;}.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);}}.carrier{display:flex;align-items:center;gap:var(--otto-space-3);min-height:var(--otto-chip-height);}.airline{flex:1;min-width:0;display:flex;align-items:center;gap:var(--otto-space-4);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);}.airlineName{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.chips{display:flex;gap:var(--otto-space-1);flex-shrink:0;}.mark{width:var(--otto-mark-size);height:var(--otto-mark-size);border-radius:50%;flex-shrink:0;display:grid;place-items:center;font-size:var(--otto-text-xs);font-weight:var(--otto-weight-semibold);letter-spacing:0.3px;color:#fff;background:#5b6470;}img.mark{object-fit:contain;background:transparent;border:0;padding:0;}.fltline{display:flex;justify-content:space-between;margin-top:var(--otto-space-8);font-size:var(--otto-text-md);color:var(--otto-text-body);}.times,.ports{display:flex;align-items:center;justify-content:space-between;}.times{margin-top:var(--otto-space-2);gap:var(--otto-space-3);}.ports{margin-top:var(--otto-space-1);font-size:var(--otto-text-lg);color:var(--otto-text-body);}.clock{font-size:var(--otto-text-xl);font-weight:var(--otto-weight-semibold);white-space:nowrap;}.plus{font-size:var(--otto-text-sm);vertical-align:super;color:var(--otto-caution-text);}.rule{flex:1;height:1px;background:var(--otto-divider);}.plane{color:var(--otto-text-secondary);}.fareBlock{margin-top:var(--otto-space-6);display:flex;flex-direction:column;}.strip{height:66px;box-sizing:border-box;display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-3);padding:9px var(--otto-space-5) 0;border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);font-size:var(--otto-text-base);}.strip + .fareRow{margin-top:-27px;}.lowest{min-width:0;font-weight:var(--otto-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.lowestName{font-weight:var(--otto-weight-regular);color:var(--otto-text-secondary);}.strip .link{flex-shrink:0;display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:var(--otto-text-base);line-height:var(--otto-line);color:var(--otto-text-primary);user-select:none;}.strip .link:focus-visible{outline:2px solid var(--otto-accent);outline-offset:2px;border-radius:2px;}@media (hover:hover){.opt.hasFares:hover .link{color:var(--otto-link-hover);}.opt.hasFares:hover .linkText{text-decoration:underline;}}.fareRow{position:relative;height:78px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-2) var(--otto-space-5) var(--otto-space-2) var(--otto-space-6);border-radius:var(--otto-radius-fare);background:var(--otto-fare-bg);color:var(--otto-on-fare);}.fareText{min-width:0;}.amount{font-size:var(--otto-text-2xl);font-weight:var(--otto-weight-semibold);}.fareName{font-size:var(--otto-text-base);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.fareFacts{font-size:var(--otto-text-md);opacity:0.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.tag{font-size:var(--otto-text-md);}.fareRow .btn.onFare{padding:9px 20px;background:var(--otto-on-fare);color:var(--otto-fare-bg);border-color:var(--otto-on-fare);}.fareRow .btn.onFare.chosen{padding:9px var(--otto-space-7);border-color:var(--otto-accent);}.opt.hasFares{cursor:pointer;}.opt.open{border-color:var(--otto-accent);}.fareList{margin-top:var(--otto-space-7);padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);display:flex;flex-direction:column;gap:var(--otto-space-3);}.fareListHead{margin-bottom:var(--otto-space-1);font-size:var(--otto-text-md);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderRow{display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-3) var(--otto-space-3) var(--otto-space-3) var(--otto-space-6);border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);}.ladderRow.recommended{border-color:var(--otto-text-primary);}@media (hover:hover){.ladderRow:hover{border-color:var(--otto-accent);}}.ladderLabel{min-width:0;}.ladderName{font-size:var(--otto-text-base);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderFacts{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.ladderRight{flex-shrink:0;display:flex;align-items:center;gap:var(--otto-space-5);}.ladderAmount{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.ladderRow .btn{min-height:36px;padding:6px var(--otto-space-6);background:var(--otto-card-bg);border-color:var(--otto-text-primary);}.btn.chosen:not(.onFare){background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}#root.busy .opt.open:not(.chosen){border-color:var(--otto-accent);}#root.busy .opt.hasFares:hover .link{color:var(--otto-text-primary);}#root.busy .opt.hasFares:hover .linkText{text-decoration:none;}#root.busy .ladderRow:hover{border-color:var(--otto-divider);}#root.busy .ladderRow.recommended:hover{border-color:var(--otto-text-primary);}#root.busy .strip .link{cursor:default;opacity:0.45;}.btn.chosen:disabled{opacity:1;}
package/flight-card.js CHANGED
@@ -10,7 +10,7 @@
10
10
  (function () {
11
11
  "use strict";
12
12
 
13
- 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;}.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);}}.carrier{display:flex;align-items:center;gap:var(--otto-space-3);min-height:var(--otto-chip-height);}.airline{flex:1;min-width:0;display:flex;align-items:center;gap:var(--otto-space-4);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);}.airlineName{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.chips{display:flex;gap:var(--otto-space-1);flex-shrink:0;}.mark{width:var(--otto-mark-size);height:var(--otto-mark-size);border-radius:50%;flex-shrink:0;display:grid;place-items:center;font-size:var(--otto-text-xs);font-weight:var(--otto-weight-semibold);letter-spacing:0.3px;color:#fff;background:#5b6470;}img.mark{object-fit:contain;background:transparent;border:0;padding:0;}.fltline{display:flex;justify-content:space-between;margin-top:var(--otto-space-8);font-size:var(--otto-text-md);color:var(--otto-text-body);}.times,.ports{display:flex;align-items:center;justify-content:space-between;}.times{margin-top:var(--otto-space-2);gap:var(--otto-space-3);}.ports{margin-top:var(--otto-space-1);font-size:var(--otto-text-lg);color:var(--otto-text-body);}.clock{font-size:var(--otto-text-xl);font-weight:var(--otto-weight-semibold);white-space:nowrap;}.plus{font-size:var(--otto-text-sm);vertical-align:super;color:var(--otto-caution-text);}.rule{flex:1;height:1px;background:var(--otto-divider);}.plane{color:var(--otto-text-secondary);}.fareBlock{margin-top:var(--otto-space-6);display:flex;flex-direction:column;}.strip{height:66px;box-sizing:border-box;display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-3);padding:9px var(--otto-space-5) 0;border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);font-size:var(--otto-text-base);}.strip + .fareRow{margin-top:-27px;}.lowest{min-width:0;font-weight:var(--otto-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.lowestName{font-weight:var(--otto-weight-regular);color:var(--otto-text-secondary);}.strip button.link{flex-shrink:0;display:inline-flex;gap:6px;height:auto;min-height:0;margin:0;padding:0;border:0;border-radius:0;box-shadow:none;background:none;appearance:none;cursor:pointer;align-items:center;font-size:var(--otto-text-base);line-height:var(--otto-line);color:var(--otto-text-primary);}@media (hover:hover){.opt.hasFares:hover .link{color:var(--otto-link-hover);}.opt.hasFares:hover .linkText{text-decoration:underline;}}.fareRow{position:relative;height:78px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-2) var(--otto-space-5) var(--otto-space-2) var(--otto-space-6);border-radius:var(--otto-radius-fare);background:var(--otto-fare-bg);color:var(--otto-on-fare);}.fareText{min-width:0;}.amount{font-size:var(--otto-text-2xl);font-weight:var(--otto-weight-semibold);}.fareName{font-size:var(--otto-text-base);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.fareFacts{font-size:var(--otto-text-md);opacity:0.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.tag{font-size:var(--otto-text-md);}.fareRow .btn.onFare{padding:9px 20px;background:var(--otto-on-fare);color:var(--otto-fare-bg);border-color:var(--otto-on-fare);}.fareRow .btn.onFare.chosen{padding:9px var(--otto-space-7);border-color:var(--otto-accent);}.opt.hasFares{cursor:pointer;}.opt.open{border-color:var(--otto-accent);}.fareList{margin-top:var(--otto-space-7);padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);display:flex;flex-direction:column;gap:var(--otto-space-3);}.fareListHead{margin-bottom:var(--otto-space-1);font-size:var(--otto-text-md);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderRow{display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-3) var(--otto-space-3) var(--otto-space-3) var(--otto-space-6);border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);}.ladderRow.recommended{border-color:var(--otto-text-primary);}@media (hover:hover){.ladderRow:hover{border-color:var(--otto-accent);}}.ladderLabel{min-width:0;}.ladderName{font-size:var(--otto-text-base);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderFacts{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.ladderRight{flex-shrink:0;display:flex;align-items:center;gap:var(--otto-space-5);}.ladderAmount{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.ladderRow .btn{min-height:36px;padding:6px var(--otto-space-6);background:var(--otto-card-bg);border-color:var(--otto-text-primary);}.btn.chosen:not(.onFare){background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}#root.busy .opt.open:not(.chosen){border-color:var(--otto-accent);}#root.busy .opt.hasFares:hover .link{color:var(--otto-text-primary);}#root.busy .opt.hasFares:hover .linkText{text-decoration:none;}#root.busy .ladderRow:hover{border-color:var(--otto-divider);}#root.busy .ladderRow.recommended:hover{border-color:var(--otto-text-primary);}.strip button.link:disabled{cursor:default;opacity:0.45;}.btn.chosen:disabled{opacity:1;}";
13
+ 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;}.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);}}.carrier{display:flex;align-items:center;gap:var(--otto-space-3);min-height:var(--otto-chip-height);}.airline{flex:1;min-width:0;display:flex;align-items:center;gap:var(--otto-space-4);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);}.airlineName{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.chips{display:flex;gap:var(--otto-space-1);flex-shrink:0;}.mark{width:var(--otto-mark-size);height:var(--otto-mark-size);border-radius:50%;flex-shrink:0;display:grid;place-items:center;font-size:var(--otto-text-xs);font-weight:var(--otto-weight-semibold);letter-spacing:0.3px;color:#fff;background:#5b6470;}img.mark{object-fit:contain;background:transparent;border:0;padding:0;}.fltline{display:flex;justify-content:space-between;margin-top:var(--otto-space-8);font-size:var(--otto-text-md);color:var(--otto-text-body);}.times,.ports{display:flex;align-items:center;justify-content:space-between;}.times{margin-top:var(--otto-space-2);gap:var(--otto-space-3);}.ports{margin-top:var(--otto-space-1);font-size:var(--otto-text-lg);color:var(--otto-text-body);}.clock{font-size:var(--otto-text-xl);font-weight:var(--otto-weight-semibold);white-space:nowrap;}.plus{font-size:var(--otto-text-sm);vertical-align:super;color:var(--otto-caution-text);}.rule{flex:1;height:1px;background:var(--otto-divider);}.plane{color:var(--otto-text-secondary);}.fareBlock{margin-top:var(--otto-space-6);display:flex;flex-direction:column;}.strip{height:66px;box-sizing:border-box;display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-3);padding:9px var(--otto-space-5) 0;border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);font-size:var(--otto-text-base);}.strip + .fareRow{margin-top:-27px;}.lowest{min-width:0;font-weight:var(--otto-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.lowestName{font-weight:var(--otto-weight-regular);color:var(--otto-text-secondary);}.strip .link{flex-shrink:0;display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:var(--otto-text-base);line-height:var(--otto-line);color:var(--otto-text-primary);user-select:none;}.strip .link:focus-visible{outline:2px solid var(--otto-accent);outline-offset:2px;border-radius:2px;}@media (hover:hover){.opt.hasFares:hover .link{color:var(--otto-link-hover);}.opt.hasFares:hover .linkText{text-decoration:underline;}}.fareRow{position:relative;height:78px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-2) var(--otto-space-5) var(--otto-space-2) var(--otto-space-6);border-radius:var(--otto-radius-fare);background:var(--otto-fare-bg);color:var(--otto-on-fare);}.fareText{min-width:0;}.amount{font-size:var(--otto-text-2xl);font-weight:var(--otto-weight-semibold);}.fareName{font-size:var(--otto-text-base);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.fareFacts{font-size:var(--otto-text-md);opacity:0.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.tag{font-size:var(--otto-text-md);}.fareRow .btn.onFare{padding:9px 20px;background:var(--otto-on-fare);color:var(--otto-fare-bg);border-color:var(--otto-on-fare);}.fareRow .btn.onFare.chosen{padding:9px var(--otto-space-7);border-color:var(--otto-accent);}.opt.hasFares{cursor:pointer;}.opt.open{border-color:var(--otto-accent);}.fareList{margin-top:var(--otto-space-7);padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);display:flex;flex-direction:column;gap:var(--otto-space-3);}.fareListHead{margin-bottom:var(--otto-space-1);font-size:var(--otto-text-md);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderRow{display:flex;align-items:center;justify-content:space-between;gap:var(--otto-space-4);padding:var(--otto-space-3) var(--otto-space-3) var(--otto-space-3) var(--otto-space-6);border:1px solid var(--otto-divider);border-radius:var(--otto-radius-md);background:var(--otto-strip-bg);}.ladderRow.recommended{border-color:var(--otto-text-primary);}@media (hover:hover){.ladderRow:hover{border-color:var(--otto-accent);}}.ladderLabel{min-width:0;}.ladderName{font-size:var(--otto-text-base);font-weight:var(--otto-weight-semibold);color:var(--otto-text-primary);}.ladderFacts{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.ladderRight{flex-shrink:0;display:flex;align-items:center;gap:var(--otto-space-5);}.ladderAmount{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.ladderRow .btn{min-height:36px;padding:6px var(--otto-space-6);background:var(--otto-card-bg);border-color:var(--otto-text-primary);}.btn.chosen:not(.onFare){background:var(--otto-text-primary);color:var(--otto-card-bg);border-color:var(--otto-text-primary);}#root.busy .opt{cursor:default;}#root.busy .opt:not(.chosen):hover{border-color:var(--otto-card-border);}#root.busy .opt.open:not(.chosen){border-color:var(--otto-accent);}#root.busy .opt.hasFares:hover .link{color:var(--otto-text-primary);}#root.busy .opt.hasFares:hover .linkText{text-decoration:none;}#root.busy .ladderRow:hover{border-color:var(--otto-divider);}#root.busy .ladderRow.recommended:hover{border-color:var(--otto-text-primary);}#root.busy .strip .link{cursor:default;opacity:0.45;}.btn.chosen:disabled{opacity:1;}";
14
14
  const SYMBOLS = { USD: "$", EUR: "€", GBP: "£", JPY: "¥", CNY: "¥", AUD: "A$", CAD: "C$" };
15
15
  // The widget cannot see when the host's reply ends, so the lock lifts on a timer.
16
16
  const BUSY_MS = 60000;
@@ -143,7 +143,12 @@
143
143
  const strip = el("div", "strip");
144
144
  const lowest = el("span", "lowest");
145
145
  if (cheapest) lowest.append(`from ${money(cheapest.price)}`, el("span", "lowestName", ` for ${cheapest.name}`));
146
- const link = el("button", "link");
146
+ // A span, not a <button>: host sandboxes restyle every button (border, padding,
147
+ // min-height), which boxed the link and pushed it off the "from $…" line.
148
+ const link = el("span", "link");
149
+ link.setAttribute("role", "button");
150
+ link.tabIndex = 0;
151
+ link.setAttribute("aria-expanded", String(open === option.id));
147
152
  link.dataset.fares = option.id;
148
153
  link.append(el("span", "linkText", open === option.id ? "Hide fares" : "Other fares"), el("span", null, open === option.id ? "×" : "→"));
149
154
  strip.append(lowest, link);
@@ -208,6 +213,7 @@
208
213
  if (list) root.append(list);
209
214
  root.classList.toggle("busy", busy);
210
215
  root.querySelectorAll("button").forEach((b) => (b.disabled = busy));
216
+ root.querySelectorAll(".link").forEach((l) => l.setAttribute("aria-disabled", String(busy)));
211
217
  }
212
218
 
213
219
  function unlock() {
@@ -248,6 +254,14 @@
248
254
  draw();
249
255
  };
250
256
 
257
+ // The fare link is a span, so Enter and Space open it the way they would a button.
258
+ root.onkeydown = (event) => {
259
+ const link = event.target.closest?.(".link[data-fares]");
260
+ if (!link || (event.key !== "Enter" && event.key !== " ")) return;
261
+ event.preventDefault();
262
+ link.click();
263
+ };
264
+
251
265
  draw();
252
266
  const handle = { cancel: () => clearTimeout(timer) };
253
267
  root.__ottoCard = handle;
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@ottotheagent/mcp-card-assets",
3
- "version": "0.10.0",
4
- "description": "Renderers, styles, logos and photo placeholders for the Otto flight, hotel, room and car cards that Claude draws in its widget sandbox.",
3
+ "version": "0.12.0",
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": [
7
7
  "*-card.js",
8
8
  "*-card.css",
9
+ "seat-map.js",
10
+ "seat-map.css",
9
11
  "marks/*.png",
10
12
  "marks/cars/*.svg",
11
13
  "placeholders/*.svg",
package/seat-map.css ADDED
@@ -0,0 +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;}.sm button.smSeat{min-width:0;min-height:0;margin:0;padding:0;box-shadow:none;appearance:none;cursor:pointer;}button.smSeat:hover{border-color:var(--otto-accent);}.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;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{cursor:default;}
package/seat-map.js ADDED
@@ -0,0 +1,348 @@
1
+ (function () {
2
+ "use strict";
3
+ /* Shared core for the hotel, room and car renderers. The build wraps it with one card's
4
+ * source in a single script, and inlines that card's CSS and only the icons it uses.
5
+ *
6
+ * A card source calls mount(root, card, spec):
7
+ * spec.option(item, ui) builds one option's .opt element; ui.chosen says whether it is
8
+ * the current pick and ui.pick(label) makes its Select button.
9
+ * spec.message(item) is the text Select sends through the host's sendPrompt().
10
+ * spec.empty is the line shown when the card has no items.
11
+ * spec.bar draws the options as one wrapping row of buttons with no title, for a card
12
+ * whose details the host already shows above it.
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;}.sm button.smSeat{min-width:0;min-height:0;margin:0;padding:0;box-shadow:none;appearance:none;cursor:pointer;}button.smSeat:hover{border-color:var(--otto-accent);}.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;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{cursor:default;}";
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
+ const SYMBOLS = { USD: "$", EUR: "€", GBP: "£", JPY: "¥", CNY: "¥", AUD: "A$", CAD: "C$" };
17
+ // The widget cannot see when the host's reply ends, so the lock lifts on a timer.
18
+ const BUSY_MS = 60000;
19
+ const CHECK_ICON = "hotels/check-regular-full";
20
+ const INFO_ICON = "hotels/circle-info-regular-full";
21
+ // Placeholders ship beside this script, so they always match its package version.
22
+ const ASSET_BASE = ((document.currentScript && document.currentScript.src) || "").replace(/[^/]*$/, "");
23
+
24
+ function injectCss(id) {
25
+ if (!CSS || document.getElementById(id)) return;
26
+ const style = document.createElement("style");
27
+ style.id = id;
28
+ style.textContent = CSS;
29
+ document.head.appendChild(style);
30
+ }
31
+
32
+ function el(tag, className, text) {
33
+ const node = document.createElement(tag);
34
+ if (className) node.className = className;
35
+ if (text !== undefined && text !== null && text !== "") node.textContent = String(text);
36
+ return node;
37
+ }
38
+
39
+ function icon(names, fallback, className = "icon") {
40
+ const node = el("span", className);
41
+ node.setAttribute("aria-hidden", "true");
42
+ const markup = [names].flat().map((name) => ICONS[name]).find(Boolean);
43
+ if (markup) node.innerHTML = markup;
44
+ else node.textContent = fallback ?? "";
45
+ return node;
46
+ }
47
+
48
+ // The pick message quotes the exact price, so the model can tell apart options that round alike.
49
+ function formatExactMoney(value, currency) {
50
+ if (value === null || value === undefined) return "";
51
+ const symbol = SYMBOLS[currency];
52
+ const fraction = Number.isInteger(value) ? 0 : 2;
53
+ const amount = value.toLocaleString("en-US", { minimumFractionDigits: fraction, maximumFractionDigits: 2 });
54
+ return symbol ? `${symbol}${amount}` : `${currency} ${amount}`;
55
+ }
56
+
57
+ function formatMoney(value, currency) {
58
+ if (value === null || value === undefined) return "";
59
+ const symbol = SYMBOLS[currency];
60
+ const amount = Math.round(value).toLocaleString();
61
+ return symbol ? `${symbol}${amount}` : `${currency} ${amount}`;
62
+ }
63
+
64
+ // Supplier text flattened to one short line, quotes out, before it becomes the traveler's message.
65
+ function oneLine(value, max = 60) {
66
+ return String(value ?? "")
67
+ .replace(/["“”‘’`]/g, "'")
68
+ .replace(/[\s\u0000-\u001f]+/g, " ")
69
+ .trim()
70
+ .slice(0, max);
71
+ }
72
+
73
+ // The photo plate's stand-in: hotel, room or car. Supplier photos cannot load in host sandboxes.
74
+ function placeholder(kind) {
75
+ const img = el("img");
76
+ img.src = `${ASSET_BASE}placeholders/${kind}.svg`;
77
+ img.alt = "";
78
+ img.addEventListener("error", () => img.remove());
79
+ return img;
80
+ }
81
+
82
+ // The caller's reasons as ticked "Insights" lines, then caveats under the info mark.
83
+ function renderInsights(reasons, notes) {
84
+ if (!reasons?.length && !notes?.length) return null;
85
+ const why = el("div", "why");
86
+ why.append(el("div", "whyHead", "Insights"));
87
+ for (const [texts, className, names, glyph] of [
88
+ [reasons, "icon tick", CHECK_ICON, "✓"],
89
+ [notes, "icon tick note", INFO_ICON, "ⓘ"],
90
+ ]) {
91
+ for (const text of texts ?? []) {
92
+ const line = el("div", "whyLine");
93
+ line.append(icon(names, glyph, className), el("span", null, text));
94
+ why.append(line);
95
+ }
96
+ }
97
+ return why;
98
+ }
99
+
100
+ function mount(root, card, spec) {
101
+ // A second mount on the same root replaces the first card; its lock timer must not
102
+ // fire later and redraw over the new card.
103
+ if (root.__ottoCard) root.__ottoCard.cancel();
104
+ let chosen = null;
105
+ let busy = false;
106
+ let timer = null;
107
+
108
+ function draw() {
109
+ const scroll = root.querySelector(".row")?.scrollLeft || 0;
110
+ root.replaceChildren();
111
+ if (!card || !card.items || !card.items.length) {
112
+ root.append(el("p", "empty", spec.empty));
113
+ return;
114
+ }
115
+ const row = el("div", spec.bar ? "bar" : "row");
116
+ for (const item of card.items) {
117
+ const ui = {
118
+ chosen: chosen === item.id,
119
+ pick(label) {
120
+ const button = el("button", "btn" + (chosen === item.id ? " chosen" : ""));
121
+ button.dataset.select = item.id;
122
+ if (chosen === item.id) button.append(icon(CHECK_ICON, "✓"), document.createTextNode("Selected"));
123
+ else button.textContent = label;
124
+ return button;
125
+ },
126
+ };
127
+ row.append(spec.option(item, ui));
128
+ }
129
+ if (!spec.bar) {
130
+ const head = el("div", "head");
131
+ head.append(el("div", "title", card.title), el("div", "subtitle", card.subtitle));
132
+ root.append(head);
133
+ }
134
+ root.append(row);
135
+ row.scrollLeft = scroll;
136
+ root.classList.toggle("busy", busy);
137
+ root.querySelectorAll("button").forEach((b) => (b.disabled = busy));
138
+ }
139
+
140
+ function unlock() {
141
+ clearTimeout(timer);
142
+ busy = false;
143
+ draw();
144
+ }
145
+
146
+ // One pick at a time: the new pick replaces the old mark, then the card locks so a
147
+ // second click cannot send a second message.
148
+ // Assigned, not added: a re-mount replaces the handler instead of stacking another.
149
+ root.onclick = (event) => {
150
+ if (busy) return;
151
+ const button = event.target.closest("[data-select]");
152
+ const item = button && card.items.find((i) => i.id === button.dataset.select);
153
+ if (!item) return;
154
+ chosen = item.id;
155
+ busy = true;
156
+ draw();
157
+ timer = setTimeout(unlock, BUSY_MS);
158
+ try {
159
+ const sent = window.sendPrompt(spec.message(item));
160
+ if (sent && typeof sent.catch === "function") sent.catch(unlock);
161
+ } catch (error) {
162
+ unlock();
163
+ }
164
+ };
165
+
166
+ draw();
167
+ const handle = { cancel: () => clearTimeout(timer) };
168
+ root.__ottoCard = handle;
169
+ return handle;
170
+ }
171
+
172
+ /* Otto seat map: OttoSeatMap.render(root, card), card from render_elastic_seat_map.
173
+ * The server lays the cabin out (columns, aisles, rows aligned to columns), so any aircraft
174
+ * and cabin draws the same way. Collapsed, it shows the traveler's row and one row either
175
+ * side; Expand shows the whole cabin. Picking another seat shows Update Seat, which sends
176
+ * "Update seat to 15D (SEA → JFK)" as the traveler's message. */
177
+
178
+ function render(root, card) {
179
+ injectCss("otto-seat-map-css");
180
+ if (root.__ottoCard) root.__ottoCard.cancel();
181
+ if (!card || !card.rows || !card.rows.length) {
182
+ root.replaceChildren(el("p", "empty", "No seat map to show."));
183
+ return;
184
+ }
185
+
186
+ const seats = new Map();
187
+ card.rows.forEach((row, index) => row.seats.forEach((seat) => seat && seats.set(seat.id, { seat, index })));
188
+ const home = seats.get(card.current_seat)?.index ?? 0;
189
+ const aisles = new Set(card.aisles_after || []);
190
+ let expanded = false;
191
+ let pick = null;
192
+ let busy = false;
193
+ let timer = null;
194
+
195
+ const letter = (id) => String(id).replace(/^\d+/, "");
196
+ const cost = (seat) => (seat.price ? formatExactMoney(seat.price, card.currency || "USD") : "free");
197
+ const describe = (seat) => [seat.id, seat.type, cost(seat)].filter(Boolean).join(" · ");
198
+
199
+ function seatClass(seat) {
200
+ if (seat.id === card.current_seat) return "mine";
201
+ if (seat.id === pick) return "pick";
202
+ return { free: "free", paid: "paid", unavailable: "off" }[seat.state] || "off";
203
+ }
204
+
205
+ function seatNode(seat) {
206
+ const cls = seatClass(seat);
207
+ const legroom = seat.extra_legroom && cls !== "off" ? " legroom" : "";
208
+ if (cls === "off") {
209
+ const node = el("span", "smSeat off", "×");
210
+ node.setAttribute("role", "img");
211
+ node.setAttribute("aria-label", `${seat.id} unavailable`);
212
+ node.title = `${seat.id} · unavailable`;
213
+ return node;
214
+ }
215
+ const button = el("button", `smSeat ${cls}${legroom}`, letter(seat.id));
216
+ button.type = "button";
217
+ button.dataset.seat = seat.id;
218
+ button.title = describe(seat) + (seat.extra_legroom ? " · extra legroom" : "");
219
+ button.setAttribute("aria-label", button.title);
220
+ button.setAttribute("aria-pressed", String(cls === "mine" || cls === "pick"));
221
+ return button;
222
+ }
223
+
224
+ function lettersRow() {
225
+ const line = el("div", "smRow");
226
+ line.append(el("span", "smNum"));
227
+ card.columns.forEach((name, i) => {
228
+ line.append(el("span", "smLetter", name));
229
+ if (aisles.has(i)) line.append(el("span", "smAisle"));
230
+ });
231
+ return line;
232
+ }
233
+
234
+ function seatRow(row) {
235
+ const line = el("div", "smRow");
236
+ const number = el("span", "smNum" + (row.exit ? " exit" : ""), row.number);
237
+ if (row.exit) number.title = "Exit row";
238
+ line.append(number);
239
+ row.seats.forEach((seat, i) => {
240
+ line.append(seat ? seatNode(seat) : el("span", "smEmpty"));
241
+ if (aisles.has(i)) line.append(el("span", "smAisle"));
242
+ });
243
+ return line;
244
+ }
245
+
246
+ // Swatches reuse the seat classes, so the legend shows exactly what the map draws.
247
+ function legend() {
248
+ const states = new Set([...seats.values()].map(({ seat }) => seat.state));
249
+ const keys = [
250
+ ["mine", "Your seat", true],
251
+ ["pick", "New pick", true],
252
+ ["free", "Free", states.has("free")],
253
+ ["paid", "Paid", states.has("paid")],
254
+ ["off", "Unavailable", states.has("unavailable")],
255
+ ];
256
+ const box = el("div", "smLegend");
257
+ for (const [cls, label, shown] of keys) {
258
+ if (!shown) continue;
259
+ const key = el("span", "smKey");
260
+ const swatch = el("span", `smSeat ${cls}`, cls === "off" ? "×" : "");
261
+ swatch.setAttribute("aria-hidden", "true");
262
+ key.append(swatch, el("span", null, label));
263
+ box.append(key);
264
+ }
265
+ if ([...seats.values()].some(({ seat }) => seat.extra_legroom)) {
266
+ const key = el("span", "smKey");
267
+ key.append(el("span", "smSeat free legroom"), el("span", null, "Extra legroom"));
268
+ box.append(key);
269
+ }
270
+ return box;
271
+ }
272
+
273
+ function draw() {
274
+ const scroll = root.querySelector(".smScroll")?.scrollLeft || 0;
275
+ const box = el("div", `sm ${card.size || "standard"}${busy ? " busy" : ""}`);
276
+
277
+ const head = el("div", "smHead");
278
+ const text = el("div");
279
+ text.append(el("div", "title", card.title), el("div", "subtitle", card.subtitle));
280
+ head.append(text);
281
+ if (card.rows.length > 3) {
282
+ const toggle = el("button", "btn smToggle", expanded ? "Collapse" : "Expand");
283
+ toggle.type = "button";
284
+ toggle.dataset.toggle = "1";
285
+ toggle.setAttribute("aria-expanded", String(expanded));
286
+ head.append(toggle);
287
+ }
288
+
289
+ const rows = expanded ? card.rows : card.rows.slice(Math.max(0, home - 1), home + 2);
290
+ const grid = el("div", "smGrid");
291
+ grid.append(lettersRow(), ...rows.map(seatRow));
292
+ const scroller = el("div", "smScroll");
293
+ scroller.append(grid);
294
+
295
+ const foot = el("div", "smFoot");
296
+ const mine = seats.get(card.current_seat)?.seat;
297
+ const chosen = pick && seats.get(pick)?.seat;
298
+ foot.append(el("span", null, chosen ? `New seat: ${describe(chosen)}` : mine ? `Your seat: ${describe(mine)}` : ""));
299
+ if (chosen) {
300
+ const update = el("button", "btn", `Update seat · ${chosen.id} · ${cost(chosen)}`);
301
+ update.type = "button";
302
+ update.dataset.update = chosen.id;
303
+ foot.append(update);
304
+ }
305
+
306
+ box.append(head, scroller, legend(), foot);
307
+ root.replaceChildren(box);
308
+ scroller.scrollLeft = scroll;
309
+ root.querySelectorAll("button").forEach((b) => (b.disabled = busy));
310
+ }
311
+
312
+ function unlock() {
313
+ clearTimeout(timer);
314
+ busy = false;
315
+ draw();
316
+ }
317
+
318
+ // Assigned, not added: a re-mount replaces the handler instead of stacking another.
319
+ root.onclick = (event) => {
320
+ if (busy) return;
321
+ const target = event.target.closest("button");
322
+ if (!target) return;
323
+ if (target.dataset.toggle) {
324
+ expanded = !expanded;
325
+ } else if (target.dataset.seat) {
326
+ const id = target.dataset.seat;
327
+ pick = id === card.current_seat || id === pick ? null : id;
328
+ } else if (target.dataset.update) {
329
+ busy = true;
330
+ timer = setTimeout(unlock, BUSY_MS);
331
+ try {
332
+ const sent = window.sendPrompt(`Update seat to ${target.dataset.update} (${card.route})`);
333
+ if (sent && typeof sent.catch === "function") sent.catch(unlock);
334
+ } catch (error) {
335
+ unlock();
336
+ return;
337
+ }
338
+ }
339
+ draw();
340
+ };
341
+
342
+ draw();
343
+ root.__ottoCard = { cancel: () => clearTimeout(timer) };
344
+ }
345
+
346
+ window.OttoSeatMap = { render };
347
+
348
+ })();