/* Portal — a ruled board. Modules sit on a hairline grid like a printed
     timetable; color is reserved for data (up/down) and interaction. */
  :root{
    --paper:#f2f2ef;
    --sheet:#fbfbfa;
    --sheet-2:#f0f0ed;
    --line:#dedbd4;
    --ink:#15171b;
    --ink-2:#5c6069;
    --ink-3:#8e929b;
    --accent:#2340c8;
    --accent-ink:#ffffff;
    --wash:#e8ebf9;
    --pos:#0d7a4d;
    --neg:#bc3126;
  }
  @media (prefers-color-scheme: dark){
    :root:not([data-theme="light"]){
      --paper:#0a0b0d;
      --sheet:#131519;
      --sheet-2:#1a1d22;
      --line:#262a31;
      --ink:#e9eaee;
      --ink-2:#969ba5;
      --ink-3:#6a6f79;
      --accent:#8b9bff;
      --accent-ink:#0a0b0d;
      --wash:#1a1f33;
      --pos:#42cf90;
      --neg:#ff6f60;
    }
  }
  :root[data-theme="dark"]{
    --paper:#0a0b0d; --sheet:#131519; --sheet-2:#1a1d22; --line:#262a31;
    --ink:#e9eaee; --ink-2:#969ba5; --ink-3:#6a6f79;
    --accent:#8b9bff; --accent-ink:#0a0b0d; --wash:#1a1f33;
    --pos:#42cf90; --neg:#ff6f60;
  }

  *{box-sizing:border-box;}
  html{-webkit-text-size-adjust:100%;}
  body{margin:0;background:var(--paper);color:var(--ink);
    font-family:'Archivo',ui-sans-serif,system-ui,sans-serif;font-size:13px;line-height:1.45;
    -webkit-font-smoothing:antialiased;}
  .mono{font-family:'IBM Plex Mono',ui-monospace,'SF Mono',monospace;font-variant-numeric:tabular-nums;
    font-feature-settings:"zero" 1;}
  a{color:inherit;}
  button{font-family:inherit;}

  .app{max-width:1280px;margin:0 auto;
    padding:0 calc(20px + env(safe-area-inset-right)) 80px calc(20px + env(safe-area-inset-left));}

  /* ---- masthead ---- */
  /* added to a home screen there is no browser chrome above this, so the bar
     has to keep itself clear of the status bar on its own */
  .topbar{display:flex;align-items:center;gap:10px;
    padding:calc(30px + env(safe-area-inset-top)) 0 18px;}
  /* The title carries the navigation: "Portal" goes home, the slash after it
     opens the index of named boards. Two targets in one word, so each has to
     answer to the pointer on its own or neither reads as pressable. */
  .wordmark{margin:0;display:flex;align-items:baseline;
    font-size:34px;font-weight:700;letter-spacing:-0.035em;line-height:1;
    color:var(--ink);text-decoration:none;}
  .mark-name{color:inherit;text-decoration:none;}
  .mark-name:hover{color:var(--accent);}
  .mark-path{display:inline-flex;align-items:baseline;
    font:inherit;letter-spacing:inherit;background:none;border:0;padding:0;
    color:var(--ink-3);cursor:pointer;border-bottom:2px solid transparent;}
  .mark-path:hover,.mark-path[aria-expanded="true"]{color:var(--accent);border-bottom-color:var(--accent);}
  .wordmark .mark-path,
  .wordmark .mark-slug{font-family:'IBM Plex Mono',monospace;font-weight:500;}

  /* Not a dropdown. The masthead opens downward into an index ruled like the
     board it sits above, and the board moves down to make room for it. */
  .path-index{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));
    column-gap:34px;margin-top:2px;margin-bottom:24px;border-top:2px solid var(--ink);}
  .path-index[hidden]{display:none;}
  .path-item{display:flex;flex-direction:column;gap:2px;justify-content:flex-start;
    padding:9px 0 10px;border-bottom:1px solid var(--line);
    text-decoration:none;color:var(--ink);}
  .path-item b{font-family:'IBM Plex Mono',monospace;font-size:13px;font-weight:600;
    letter-spacing:-0.01em;}
  .path-item small{font-size:11.5px;line-height:1.5;color:var(--ink-3);}
  .path-item:hover{border-bottom-color:var(--ink);}
  .path-item:hover b{color:var(--accent);}
  .path-item:hover small{color:var(--ink-2);}
  
  .topbar .spacer{flex:1;}
  .now{font-family:'IBM Plex Mono',monospace;font-size:15px;font-variant-numeric:tabular-nums;
    color:var(--ink-2);letter-spacing:-0.01em;}
  .count{font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-3);}

  .btn{font-size:12px;font-weight:600;border:1px solid var(--line);background:transparent;color:var(--ink);
    padding:6px 11px;border-radius:0;cursor:pointer;}
  .btn:hover{border-color:var(--ink-3);}
  .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink);}
  .btn:focus-visible,.icon-btn:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,.add-panel button:focus-visible,.combo-opt:focus-visible{
    outline:2px solid var(--accent);outline-offset:1px;}

  .add-menu{position:relative;}
  .add-panel{position:absolute;right:0;top:calc(100% + 6px);min-width:210px;z-index:220;
    background:var(--sheet);border:1px solid var(--line);border-radius:0;
    box-shadow:0 10px 30px rgba(0,0,0,0.16);padding:4px;display:flex;flex-direction:column;}
  .add-panel[hidden]{display:none;}
  .add-panel button{text-align:left;background:none;border:none;padding:8px 9px;font-size:12.5px;color:var(--ink);border-radius:0;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:10px;}
  .add-panel button:hover{background:var(--sheet-2);}
  .add-panel button:disabled{color:var(--ink-3);cursor:default;}
  .add-panel button:disabled:hover{background:none;}

  .notice{display:flex;gap:9px;margin-top:14px;padding:10px 12px;font-size:12.5px;line-height:1.55;
    color:var(--ink-2);background:var(--wash);border-radius:0;}
  .notice[hidden]{display:none;}
  .notice b{color:var(--ink);font-weight:600;}
  .notice code{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink);}

  /* ---- board: columns of blocks, each opened by a rule ---- */
  /* one row of widgets at a time; the tallest in a row sets its height and
     the others stretch to match, so their edges line up */
  .board-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));
    column-gap:34px;row-gap:30px;align-items:stretch;}
  .module{display:flex;flex-direction:column;border-top:2px solid var(--ink);transition:border-top-color .12s;position:relative;}
  /* the rule at the top is the handle: hovering the widget colours it, and the
     header under it is what the drag starts on */
  .module:hover{border-top-color:var(--accent);}
  /* wide = its own row, content laid out across the page */
  .module.full{grid-column:1/-1;}
  /* the middle width: two columns of the board, wherever the board has three
     or more. What no longer fits beside it is pushed on by the grid itself. */
  .module.mid{grid-column:span 2;}
  /* the other axis: two rows, so a feed can be read instead of scrolled. The
     body fills the taller cell and keeps its own scrollbar past that. */
  .module.tall{grid-row:span 2;}
  /* two rows has to be worth asking for even when the widget is alone in them,
     so the body claims the height rather than waiting for a neighbour to set it */
  .module.tall .mod-body{flex:1 1 auto;min-height:330px;}
  /* align-items:start puts each item at the top of its row; align-content:start
     is what stops the *rows* sharing out height the widget was given but does
     not need. Without it a tall neighbour spread six headlines over 500px,
     a hand apart, which reads as a broken list rather than a generous one. */
  .module.wide .mod-body{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));column-gap:34px;row-gap:0;align-items:start;align-content:start;max-height:420px;}
  .module.wide .mod-body > form,
  .module.wide .mod-body > .section-label,
  .module.wide .mod-body > .subdivider,
  .module.wide .mod-body > .clock-line,
  .module.wide .mod-body > .trip-grid,
  .module.wide .mod-body > .wk-tabs,
  .module.wide .mod-body > .empty-hint,
  .module.wide .mod-body > .loading,
  .module.wide .mod-body > .err,
  .module.wide .mod-body > .threshold-note,
  .module.wide .mod-body > .chart-head,
  .module.wide .mod-body > .candles-box,
  .module.wide .mod-body > .chart-read,
  .module.wide .mod-body > .today,
  /* A widget whose body is one thing rather than a list of them: a tape has to
     run the width, a table has to have it, and a curve drawn into a fifth of it
     is a curve nobody can read. */
  .module.wide .mod-body > .tape,
  .module.wide .mod-body > .xr-wrap,
  .module.wide .mod-body > .curve,
  .module.wide .mod-body > .perk-tally,
  .module.wide .mod-body > textarea{grid-column:1/-1;}
  .module.wide .row:first-child{border-top:1px solid var(--line);}
  .mod-actions{display:flex;gap:1px;flex:0 0 auto;}
  .module.dragging{opacity:0.35;background:var(--wash);border-top-color:var(--accent);}

  /* The header is the handle, so it says so on hover: everything under it is
     text you may want to select rather than a thing to pick up. */
  /* The same two rows everywhere: the title and the widget's controls on one
     line, and whatever the widget has to say for itself — its tabs, or what a
     feed is reading — on its own line under the title. The board used to run
     all three across one line, so a widget with tabs read as a different
     component from one without. Full screen already did it this way. */
  .mod-head{padding:8px 0 10px;cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;column-gap:12px;row-gap:8px;min-height:40px;position:relative;}
  .mod-head > .mod-title{grid-column:1;grid-row:1;display:flex;align-items:center;gap:8px;min-width:0;}
  .mod-head > .mod-actions{grid-column:2;grid-row:1;display:flex;align-items:center;gap:2px;justify-self:end;}
  .mod-head > .mod-sub{grid-column:1 / -1;grid-row:2;min-width:0;display:flex;align-items:center;overflow:hidden;}
  .mod-head > .mod-sub:empty{display:none;}
  .mod-head:active{cursor:grabbing;}
  /* The handle, on the top edge in the middle, where a thing you pick up is
     picked up from. It pops down out of the rule when the widget is hovered —
     overshooting a little, so it reads as arriving rather than fading in — and
     takes no pointer events of its own: the header underneath it is the
     handle, and this is the part that says so. */
  @media (prefers-reduced-motion: reduce){
    
  }
  .mod-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.14em;color:var(--ink);}
  .icon-btn{background:none;border:none;color:var(--ink-3);cursor:pointer;font-size:13px;line-height:1;
    width:22px;height:22px;border-radius:0;display:flex;align-items:center;justify-content:center;flex:none;
    opacity:0;transition:opacity .12s;}
  .module:hover .icon-btn,.icon-btn:focus-visible{opacity:1;}
  .icon-btn:hover{background:var(--sheet-2);color:var(--ink);}
  /* content grows to a ceiling, then scrolls inside the widget so one long
     list cannot stretch its whole row */
  .mod-body{padding:0;display:flex;flex-direction:column;gap:10px;
    max-height:300px;overflow-y:auto;overscroll-behavior:contain;
    scrollbar-width:thin;scrollbar-color:var(--line) transparent;}
  .mod-body::-webkit-scrollbar{width:5px;}
  .mod-body::-webkit-scrollbar-thumb{background:var(--line);border-radius:0;}
  /* editing opens pickers that must escape the box; zoom is meant to show it all */
  .module.editing .mod-body,
  .board-grid.zoomed .mod-body{max-height:none;overflow:visible;}

  /* ---- data rows ---- */
  .row{display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--line);}
  .row:first-child{border-top:none;}
  .row-main{flex:1;min-width:0;}
  .row-title{font-weight:600;font-size:13px;letter-spacing:-0.005em;}
  .row-sub{font-size:11.5px;color:var(--ink-3);}
  .row-value{text-align:right;}
  .price{font-family:'IBM Plex Mono',monospace;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;}
  .delta{font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:500;font-variant-numeric:tabular-nums;}
  .delta.up{color:var(--pos);}
  .delta.down{color:var(--neg);}
  
  .row.cheapest{background:var(--wash);margin:0;padding:7px 8px;}
  .badge{font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;
    color:var(--accent);margin-left:6px;}
  .book{font-size:11px;font-weight:600;color:var(--accent);text-decoration:none;white-space:nowrap;}
  /* two ways out of a fare: buy it, or find out whether today is the day */
  .fare-links{display:flex;flex-direction:column;align-items:flex-end;gap:1px;}
  .book.quiet{color:var(--ink-3);font-weight:500;}
  .book.quiet:hover{color:var(--accent);}
  .book:hover{text-decoration:underline;}
  svg.spark{display:block;overflow:visible;flex:none;}

  .section-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.11em;
    color:var(--ink-3);margin-top:2px;}
  .subdivider{border:none;border-top:1px solid var(--line);margin:4px 0;}
  .empty-hint,.loading{font-size:12.5px;color:var(--ink-3);padding:4px 0;}
  .err{font-size:12px;color:var(--neg);line-height:1.55;padding:2px 0;}
  .threshold-note{font-size:11px;color:var(--ink-3);}

  /* ---- forms ---- */
  .inline-add{display:flex;gap:5px;flex-wrap:wrap;margin-top:4px;}
  .inline-add input,.inline-add select{font-family:inherit;font-size:12.5px;padding:6px 8px;border-radius:0;
    border:1px solid var(--line);background:var(--sheet-2);color:var(--ink);min-width:0;flex:1;}
  .inline-add input::placeholder{color:var(--ink-3);}
  /* a date field needs a fixed amount of room and no more; letting it take a
     share of the slack as well starves the text field beside it until its own
     placeholder is cut in half */
  .inline-add input[type="date"]{flex:0 0 auto;padding-left:7px;padding-right:5px;}
  /* the browser's own picker button is generously padded; a widget column is not */
  .inline-add input[type="date"]::-webkit-calendar-picker-indicator{
    margin-left:3px;padding:0;opacity:0.55;}
  .inline-add input[type="date"]::-webkit-calendar-picker-indicator:hover{opacity:1;}
  .inline-add button{font-size:12.5px;font-weight:600;padding:6px 11px;border-radius:0;
    border:1px solid var(--accent);background:var(--accent);color:var(--accent-ink);cursor:pointer;white-space:nowrap;}

  /* ---- airport picker ---- */
  .combo{position:relative;flex:1;min-width:160px;}
  .combo input{width:100%;font-family:inherit;font-size:12.5px;padding:6px 8px;border-radius:0;
    border:1px solid var(--line);background:var(--sheet-2);color:var(--ink);}
  .combo-list{position:absolute;left:0;right:0;top:calc(100% + 3px);z-index:80;max-height:250px;overflow-y:auto;
    background:var(--sheet);border:1px solid var(--line);border-radius:0;box-shadow:0 10px 30px rgba(0,0,0,0.2);}
  .combo-list[hidden]{display:none;}
  .combo-opt{display:flex;align-items:baseline;gap:8px;padding:7px 10px;cursor:pointer;}
  .combo-opt.active,.combo-opt:hover{background:var(--wash);}
  .combo-opt .opt-code{font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:600;
    color:var(--accent);flex:none;width:32px;}
  .combo-opt .opt-city{font-size:12.5px;font-weight:600;}
  .combo-opt .opt-where{font-size:11px;color:var(--ink-3);margin-left:auto;white-space:nowrap;}
  .combo-empty{padding:8px 10px;font-size:12px;color:var(--ink-3);}
  .picked{display:flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;
    background:var(--sheet-2);border:1px solid var(--line);border-radius:0;padding:5px 7px;}
  .picked .code{font-family:'IBM Plex Mono',monospace;color:var(--accent);}
  .picked .icon-btn{opacity:1;width:18px;height:18px;}

  .arrow{color:var(--ink-3);align-self:center;font-size:12px;}

  /* ---- home ---- */

  .guess{font-size:10px;font-weight:500;text-transform:uppercase;letter-spacing:0.08em;color:var(--ink-3);}

  /* ---- world clock: one ruled strip ---- */
  /* A strip of cities across the width: hairlines between them and nothing
     behind them. A filled tile for every city turned a row of times into a row
     of buttons, and it only has to be one line tall. */
  .clock-line{display:flex;flex-wrap:wrap;border:1px solid var(--line);}
  .clock-seg{flex:1 1 0;min-width:88px;padding:6px 10px 7px;text-align:center;
    border-left:1px solid var(--line);}
  .clock-seg:first-child{border-left:none;}
  .module.wide .clock-seg{padding:6px 10px 7px;}
  .module.wide .clock-seg .seg-time{font-size:16px;}
  .clock-seg.home .seg-city{color:var(--accent);}
  
  .clock-seg .seg-time{font-family:'IBM Plex Mono',monospace;font-size:16px;font-weight:500;
    font-variant-numeric:tabular-nums;margin-top:1px;letter-spacing:-0.02em;}
  .clock-seg .seg-day{font-size:10px;font-weight:500;color:var(--ink-3);margin-left:5px;}

  /* ---- weather ---- */
  .wx-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line);}
  .wx-row:first-child{border-top:none;}
  .wx-icon{font-size:15px;width:20px;text-align:center;flex:none;}
  .wx-place{flex:1;font-weight:600;font-size:12.5px;}
  .wx-place small{display:block;font-weight:400;color:var(--ink-3);font-size:11px;}
  .wx-temp{font-family:'IBM Plex Mono',monospace;font-size:14px;font-weight:500;font-variant-numeric:tabular-nums;}

  /* ---- to-do ---- */
  .todo-row{display:flex;align-items:center;gap:9px;padding:5px 0;border-top:1px solid var(--line);}
  .todo-row:first-child{border-top:none;}
  .todo-row input[type="checkbox"]{width:14px;height:14px;accent-color:var(--accent);flex:none;}
  .todo-text{flex:1;font-size:12.5px;}
  .todo-text.done{text-decoration:line-through;color:var(--ink-3);}
  textarea.notes{width:100%;min-height:104px;resize:vertical;font-family:'IBM Plex Mono',monospace;
    font-size:12px;line-height:1.6;border:1px solid var(--line);border-radius:0;
    background:var(--sheet-2);color:var(--ink);padding:9px;outline:none;}

  /* ---- weekend ---- */
  .wk-tabs{display:flex;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:0;overflow:hidden;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:0 0 auto;min-height:31px;align-items:stretch;}

  .trip-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1px;background:var(--line);border:1px solid var(--line);border-radius:0;overflow:hidden;align-content:start;}
  .trip{background:var(--sheet-2);padding:9px 10px;display:flex;flex-direction:column;gap:2px;min-height:66px;}
  .trip.hero{grid-column:1/-1;background:var(--wash);padding:16px;min-height:0;}
  .trip-top{display:flex;align-items:baseline;justify-content:space-between;gap:10px;}
  .trip-dest{font-size:13px;font-weight:600;}
  .trip.hero .trip-dest{font-size:24px;letter-spacing:-0.02em;}
  .trip-price{font-family:'IBM Plex Mono',monospace;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;}
  .trip.hero .trip-price{font-size:28px;letter-spacing:-0.03em;}
  .trip-meta{font-size:11px;color:var(--ink-3);}
  .trip.hero .trip-meta{font-size:12.5px;color:var(--ink-2);}
  .flight-route{font-family:'IBM Plex Mono',monospace;font-weight:600;letter-spacing:0.01em;}

  footer{max-width:1280px;margin:18px auto 0;padding:0 20px;font-size:11px;color:var(--ink-3);}
  @media (prefers-reduced-motion:reduce){*{transition:none!important;}}

  a.ghost{text-decoration:none;}
  .settings-page{max-width:720px;}
  .settings-page .sec{border-top:2px solid var(--ink);margin-top:30px;padding-top:12px;}
  .settings-page h2{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.14em;margin:0 0 10px;}
  .settings-page p{font-size:13px;color:var(--ink-2);line-height:1.6;margin:0 0 12px;max-width:60ch;}
  .settings-page p a{color:var(--accent);}
  .status-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
    padding:8px 0;border-top:1px solid var(--line);}
  .status-row:first-of-type{border-top:none;}
  .status-name{font-size:13px;font-weight:600;}
  .status-name small{display:block;font-weight:400;color:var(--ink-3);font-size:11.5px;}
  .status-val{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink-2);}
  .status-val.on{color:var(--pos);}
  .status-val.off{color:var(--ink-3);}
  .back{font-size:12.5px;color:var(--ink-2);text-decoration:none;}
  .mod-title{display:flex;align-items:baseline;gap:9px;min-width:0;}
  
  .trip-type{display:flex;gap:1px;background:var(--line);border:1px solid var(--line);
    border-radius:0;overflow:hidden;flex:none;}
  .trip-type input{position:absolute;opacity:0;pointer-events:none;}
  .trip-type label{font-size:11px;font-weight:600;padding:5px 9px;cursor:pointer;
    background:var(--sheet-2);color:var(--ink-3);}
  .trip-type input:checked + label{background:var(--accent);color:var(--accent-ink);}
  .trip-type input:focus-visible + label{outline:2px solid var(--accent);outline-offset:1px;}
  form[data-add="track"]:has(#tt-one:checked) .ret{display:none;}
  .locked{display:flex;gap:8px;align-items:baseline;font-size:12.5px;color:var(--ink-3);
    line-height:1.5;padding:4px 0;}
  .locked .lock{color:var(--line);font-size:8px;}
  .locked a{color:var(--accent);text-decoration:none;}
  .locked a:hover{text-decoration:underline;}

  .ghost{width:28px;height:28px;display:flex;align-items:center;justify-content:center;
    font-size:16px;line-height:1;border:none;background:none;color:var(--ink-3);
    border-radius:0;cursor:pointer;}
  .ghost:hover{background:var(--sheet-2);color:var(--ink);}
  .ghost:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
  .edit-btn{opacity:0;letter-spacing:-3px;margin-right:2px;}
  .module:hover .edit-btn,.edit-btn.on{opacity:1;}
  .edit-btn.on{color:var(--accent);}
  .module.editing{outline:1px dashed var(--line);outline-offset:6px;}
  .movers{display:flex;flex-direction:column;gap:0;flex:none;margin-right:2px;}
  .movers .icon-btn{opacity:1;width:16px;height:13px;font-size:8px;}
  .movers .icon-btn:disabled{opacity:0.25;cursor:default;}
  .movers .icon-btn:disabled:hover{background:none;}
  .grip{display:inline-flex;align-items:center;justify-content:center;flex:none;
    width:16px;letter-spacing:-3px;font-size:11px;color:var(--ink-3);
    cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;}
  .grip:active{cursor:grabbing;}
  .item-dragging{opacity:0.5;background:var(--wash);}
  .module.editing [data-sort-item]{cursor:default;}
  .module.editing .clock-seg{position:relative;}
  .module.editing .clock-seg .grip{position:absolute;top:2px;left:50%;transform:translateX(-50%);}

  /* ---- narrow screens ---- */
  @media (max-width:640px){
    .app{padding:0 14px 60px;}
    .topbar{padding:22px 0 10px;flex-wrap:wrap;gap:8px 12px;}
    .wordmark{font-size:26px;}
    /* one column, so the index is a list: tighten it or it fills the screen */
    .path-index{margin-bottom:18px;}
    .path-item{padding:7px 0 8px;}
    .path-item small{font-size:11px;}
    
    .now{font-size:13px;}
    .board-grid{grid-template-columns:1fr;column-gap:0;}
    .module{margin-bottom:24px;}
    .module.wide .mod-body{grid-template-columns:1fr;}
    .trip.hero .trip-dest{font-size:20px;}
    .trip.hero .trip-price{font-size:22px;}
    .clock-seg{min-width:70px;}
    .clock-seg .seg-time{font-size:15px;}
    
    .inline-add input,.inline-add select,.combo input{font-size:16px;} /* iOS zooms below 16 */
    .icon-btn{opacity:1;}                                              /* no hover on touch */
    .edit-btn{opacity:1;}
    .row-remove{opacity:1;}
  }
  /* weekend tabs scroll sideways rather than pushing the page wide */
  
  .wk-tabs::-webkit-scrollbar{display:none;}
  
  .edit-btn svg{display:block;}
  /* clicks should land on the button, not the glyph inside it */
  .icon-btn svg{pointer-events:none;}
  /* a swipe that starts on a header still scrolls the page; only jiggle mode
     takes the gesture away from the browser */

  /* the date, read as a clock: one axis, centred, with the year told as a
     quantity underneath rather than as a number alone */
  .today{display:flex;flex-direction:column;flex:1;min-height:0;padding:6px 0 4px;}
  /* the clock itself takes the whole widget; the switches, when they are shown,
     are pinned under it rather than pushed about by it */
  .today-main{flex:1;min-height:0;display:flex;flex-direction:column;
    align-items:center;justify-content:center;text-align:center;gap:4px;}
  .today-controls{margin-top:14px;padding-top:12px;border-top:1px solid var(--line);}
  /* the type is sized against the widget, so a bigger clock is a bigger clock */
  .today-time{font-weight:500;letter-spacing:-0.04em;line-height:1;}
  .today-date{font-weight:600;}
  .today-year{font-family:'IBM Plex Mono',monospace;letter-spacing:0.04em;
    font-size:clamp(11.5px,2.9cqw,15px);}
  /* the dial takes whatever height the widget has going spare, and its width
     follows from that, so a tall clock is a big clock rather than a small one
     with a lot of room around it */
  .today .face.today-face{flex:1 1 auto;width:auto;height:auto;max-width:100%;aspect-ratio:1;margin-bottom:6px;}
  /* the year, counted out: a line each, held apart from the clock by space
     rather than by a rule — the widget is a list of facts, not a chart */
  .today-facts{display:flex;flex-direction:column;align-items:center;gap:7px;
    font-size:clamp(12px,3cqw,15px);line-height:1.3;color:var(--ink-3);}
  .today-facts b{font-weight:600;color:var(--ink);font-size:1.08em;}
  /* the clock is not a list that scrolls: it takes the whole widget, and the
     dial takes whatever of that the words do not */
  .module.t-today .mod-body{flex:1;justify-content:center;max-height:none;overflow:visible;
    container-type:inline-size;}
  .module.wide .mod-body > .today{grid-column:1/-1;}
  .board-grid.zoomed .today .face.today-face{min-height:220px;}

  /* a row of things that are either on or off, said in words */
  .chips{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;margin-top:10px;}
  .chip{font-size:11px;font-weight:600;padding:4px 8px;border-radius:0;cursor:pointer;
    border:1px solid var(--line);background:transparent;color:var(--ink-3);}
  .chip:hover{border-color:var(--ink-3);color:var(--ink);}
  .chip.on{border-color:var(--accent);background:var(--wash);color:var(--accent);}
  
  /* Dragging a widget. The one left behind becomes the slot it will come back
     to — an empty ruled space, not a faded copy — and a copy of it rides with
     the pointer, lifted off the page so the board reads as being underneath. */
  
  .module.dragging > *{visibility:hidden;}
  body.dragging,body.dragging *{user-select:none;-webkit-user-select:none;}
  body.dragging{cursor:grabbing;}

  .drag-ghost{position:fixed;left:0;top:0;z-index:200;pointer-events:none;will-change:transform;}
  .drag-lift{background:var(--paper);padding:0 14px 14px;margin:0;
    transform-origin:50% 40%;
    animation:pickup 170ms cubic-bezier(.2,.8,.3,1) both;}
  .drag-lift .icon-btn{opacity:0;}
  @keyframes pickup{
    from{transform:none;box-shadow:0 0 0 rgba(0,0,0,0);}
    to{transform:scale(1.03) rotate(-0.5deg);
       box-shadow:0 2px 6px rgba(0,0,0,0.18),0 26px 50px rgba(0,0,0,0.45);}
  }
  /* and it puts itself down rather than vanishing */
  .drag-ghost.dropping .drag-lift{animation:none;opacity:0;transform:none;
    transition:transform 140ms cubic-bezier(.4,0,.2,1),opacity 140ms ease-out;}
  @media (prefers-reduced-motion: reduce){
    .drag-lift{animation:none;box-shadow:0 18px 40px rgba(0,0,0,0.35);}
  }
  .price.past{color:var(--ink-3);}
  .news-item{display:flex;gap:10px;padding:7px 0;border-top:1px solid var(--line);text-decoration:none;}
  .news-item:first-child{border-top:none;}
  .news-words{flex:1 1 auto;min-width:0;}
  /* the picture a feed publishes with the piece, at the size a board shows it */
  .news-shot{object-fit:cover;border-radius:0;background:var(--sheet-2);align-self:flex-start;flex:0 0 64px;width:64px;height:64px;}
  .news-title{display:block;font-size:12.5px;line-height:1.45;color:var(--ink);}
  .news-item:hover .news-title{color:var(--accent);}
  .news-meta{display:block;font-size:10.5px;color:var(--ink-3);margin-top:2px;}
  .module.wide .mod-body > .news-item:nth-child(-n+2){border-top:none;}

  /* one widget, full window */
  .board-grid.zoomed{grid-template-columns:1fr;}
  .board-grid.zoomed .module{grid-column:1/-1;min-height:calc(100vh - 150px);}
  .board-grid.zoomed .mod-label{font-size:13px;}
  .board-grid.zoomed .mod-body{font-size:14px;}
  .board-grid.zoomed .icon-btn{opacity:1;}
  .board-grid.zoomed .row{padding:11px 0;}
  .board-grid.zoomed .row-title{font-size:15px;}
  .board-grid.zoomed .price{font-size:17px;}
  .board-grid.zoomed .news-title{font-size:14.5px;}
  .board-grid.zoomed .today-time{font-size:64px;}
  .board-grid.zoomed .seg-time{font-size:26px;}
  .board-grid.zoomed .trip-grid{grid-template-columns:repeat(auto-fill,minmax(210px,1fr));}
  /* headlines scroll inside the widget rather than stretching its row */
  .wx-fc{display:flex;gap:12px;font-size:11.5px;color:var(--ink-2);white-space:nowrap;}
  .wx-fc-day small{color:var(--ink-3);margin-left:2px;}
  .mod-actions.confirm{display:flex;align-items:center;gap:7px;}
  .confirm-q{font-size:11px;color:var(--ink-2);}
  .confirm-yes,.confirm-no{font-size:11px;font-weight:600;border:1px solid var(--line);
    background:transparent;color:var(--ink);padding:3px 8px;border-radius:0;cursor:pointer;}
  .confirm-yes{border-color:var(--neg);color:var(--neg);}
  .confirm-yes:hover{background:var(--neg);color:var(--paper);}
  .confirm-no:hover{border-color:var(--ink-3);}
  
  .add-count{font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--ink-3);
    border:1px solid var(--line);border-radius:0;padding:0 4px;}
  /* a wider widget has room for more before it needs to scroll */
  
  /* and a taller one, more again: two rows is the point of asking for two rows */
  .module.tall .mod-body,.module.tall.wide .mod-body{max-height:760px;}
  /* weekend sizes itself by how many trips it shows, so it never clips a card */
  .module[data-key^="weekend"] .mod-body{max-height:none;overflow:visible;}
  /* keep the weekend header from shifting as the grid reflows */

  /* ---- 2048 ---- */
  .game-head{display:flex;align-items:baseline;gap:8px;padding-bottom:2px;}
  .game-head .price{margin-right:auto;}
  .game-new{padding:3px 9px;font-size:11px;}
  .game-grid{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(4,1fr);gap:6px;
    background:var(--line);padding:6px;border-radius:0;touch-action:none;
    aspect-ratio:1;outline:none;}
  .game-grid:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .tile{display:flex;align-items:center;justify-content:center;min-width:0;min-height:0;background:var(--sheet-2);
    border-radius:0;font-family:'IBM Plex Mono',monospace;font-weight:600;
    font-size:clamp(11px,3.4vw,20px);color:var(--ink);font-variant-numeric:tabular-nums;}
  .tile.v2,.tile.v4{background:var(--sheet-2);color:var(--ink-2);}
  .tile.v8,.tile.v16{background:var(--wash);}
  .tile.v32,.tile.v64{background:var(--accent);color:var(--accent-ink);}
  .tile.v128,.tile.v256,.tile.v512{background:var(--pos);color:var(--paper);}
  .tile.v1024,.tile.v2048{background:var(--neg);color:var(--paper);}
  
  /* the board is square and must stay whole — never scroll it */
  .module[data-key^="game"] .mod-body{max-height:none;overflow:visible;}
  .tile.pop{animation:tile-pop 150ms ease-out;}
  .tile.appear{animation:tile-appear 150ms ease-out;}
  @keyframes tile-pop{0%{transform:scale(1)}45%{transform:scale(1.14)}100%{transform:scale(1)}}
  @keyframes tile-appear{0%{transform:scale(.4);opacity:.2}100%{transform:scale(1);opacity:1}}
  @media (prefers-reduced-motion:reduce){
    .tile.pop,.tile.appear{animation:none;}
  }

  /* ---- shooter ---- */
  .arcade-mount{border-radius:0;overflow:hidden;line-height:0;}
  .arcade{width:100%;height:auto;display:block;aspect-ratio:4/3;touch-action:none;cursor:crosshair;}
  .module[data-key^="arcade"] .mod-body{max-height:none;overflow:visible;}
  .seg-city{font-size:11.5px;font-weight:600;margin-top:2px;white-space:nowrap;}
  .face{width:38px;height:38px;flex:none;}
  .clock-line.faces .clock-seg{padding:7px 6px 7px;}
  .clock-line.faces .face{width:30px;height:30px;margin:0 auto 1px;display:block;}
  .wx-row .row-remove,.clock-seg .row-remove{opacity:0;}
  .module.editing .wx-row .row-remove,.module.editing .clock-seg .row-remove{opacity:1;}

  /* ---- calendar ---- */
  .cal-head{display:flex;align-items:center;gap:6px;}
  .cal-month{font-size:12.5px;font-weight:600;flex:1;text-align:center;}
  .cal-head .icon-btn{opacity:1;font-size:15px;}
  .cal-today{padding:3px 8px;font-size:11px;}
  .cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;}
  .cal-dow{font-size:9.5px;font-weight:700;letter-spacing:.06em;color:var(--ink-3);
    text-align:center;padding-bottom:2px;}
  .cal-dow.weekend{color:var(--accent);}
  /* a month has to fit in the widget without scrolling, so a cell is a row of
     a sensible height rather than a square that grows with the width */
  .cal-cell{position:relative;min-height:34px;display:flex;align-items:center;justify-content:center;
    font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink-2);border-radius:0;}
  .cal-cell.blank{visibility:hidden;}
  .cal-cell.today{background:var(--accent);color:var(--accent-ink);font-weight:700;}
  .cal-cell.marked{color:var(--ink);font-weight:600;}
  /* a holiday is a day the world is shut, so it reads like a weekend does */
  .cal-cell.holiday{color:var(--accent);}
  .cal-cell.holiday.today{color:var(--accent-ink);}
  .cal-dot{position:absolute;bottom:3px;width:3px;height:3px;border-radius:50%;background:var(--accent);}
  .cal-cell.today .cal-dot{background:var(--accent-ink);}
  .module.wide .mod-body > .cal-head,.module.wide .mod-body > .cal-grid,
  .module.wide .mod-body > .week-grid{grid-column:1/-1;}
  .module.wide .cal-cell{min-height:46px;font-size:15px;}
  .module.wide.tall .cal-cell{min-height:64px;font-size:17px;}
  .board-grid.zoomed .cal-cell{min-height:70px;font-size:19px;}
  /* the calendar is a shape, not a list: it is never worth scrolling */
  .module.t-calendar .mod-body{max-height:none;overflow:visible;}

  /* ---- the week, for a widget that is wide but only one row tall ---- */
  .week-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:1px;
    background:var(--line);border:1px solid var(--line);}
  .week-cell{background:var(--sheet);padding:9px 8px 11px;text-align:center;min-width:0;}
  .week-cell.weekend{background:var(--sheet-2);}
  .week-dow{font-size:10px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;
    color:var(--ink-3);}
  .week-n{font-size:30px;font-weight:500;line-height:1.1;letter-spacing:-0.03em;}
  .week-mon{font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:0.08em;}
  .week-mark{margin-top:5px;font-size:11px;line-height:1.35;color:var(--accent);
    overflow-wrap:anywhere;}
  .week-cell.today{background:var(--wash);}
  .week-cell.today .week-n{color:var(--accent);font-weight:700;}
  .week-cell.today .week-dow{color:var(--accent);}
  .quote{margin:0;display:flex;flex-direction:column;gap:8px;padding:4px 0 2px;}
  .quote-text{font-size:15px;line-height:1.5;text-wrap:balance;}
  .quote-text::before{content:"\201C";}
  .quote-text::after{content:"\201D";}
  .quote-by{font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);}
  .module.wide .quote-text{font-size:17px;}
  /* a focused game announces itself on its header rule */
  .module.playing{border-top-color:var(--accent);}
  .module.playing .mod-label{color:var(--accent);}
  .playing-tag{font-size:9.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--accent-ink);background:var(--accent);border-radius:0;padding:1px 5px;margin-left:7px;cursor:default;}
  
  .tap-target{display:inline-block;width:14px;height:14px;margin-left:4px;cursor:default;}
  .hangar{display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin-top:8px;}
  .hangar-label{font-size:9.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
    color:var(--ink-3);margin-right:3px;}
  .skin{display:flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;
    letter-spacing:.04em;padding:3px 7px;border-radius:0;cursor:pointer;
    border:1px solid var(--line);background:var(--sheet-2);color:var(--ink-2);}
  .skin.on{border-color:var(--accent);color:var(--ink);}
  .skin.locked{opacity:.45;cursor:default;}
  .skin-dot{width:7px;height:7px;border-radius:50%;display:inline-block;}

  /* Chapi run — a wide strip of world, so the cat has room to see what is coming */
  .runner-mount{border-radius:0;overflow:hidden;line-height:0;}
  .runner{width:100%;height:auto;display:block;aspect-ratio:56/22;touch-action:none;cursor:pointer;}
  .module[data-key^="chapi"] .mod-body{max-height:none;overflow:visible;}

  /* ---- full-screen play -------------------------------------------------
     The offer, then the stage: game on top, thumbs on the bottom half. */

  .fs-prompt{display:none;}
  .launcher{display:none;}
  @media (pointer: coarse){
    /* the widget is only the way in: a game at this size cannot be played */
    .launcher{display:flex;flex-direction:column;gap:8px;padding:2px 0 4px;}
    .fs-prompt{
      display:flex;align-items:center;justify-content:center;gap:7px;
      width:100%;padding:13px 14px;
      background:var(--accent);color:#fff;
      border:1px solid var(--accent);border-radius:0;
      font:600 12px/1 "IBM Plex Mono",monospace;letter-spacing:.06em;
      text-transform:uppercase;cursor:pointer;
    }
    .fs-prompt:active{filter:brightness(1.15);}
  }

  /* ---- the stage: the game gets the whole screen, controls float over it */
  body.stage-open{overflow:hidden;}
  html:has(body.stage-open){overflow:hidden;}
  .stage{
    position:fixed;inset:0;z-index:400;background:#000;
    touch-action:none;user-select:none;-webkit-user-select:none;overflow:hidden;
    overscroll-behavior:none;-webkit-touch-callout:none;
    -webkit-tap-highlight-color:transparent;
  }
  .stage *{touch-action:none;-webkit-user-select:none;user-select:none;}
  .stage-inner{position:absolute;inset:0;}
  /* held upright, Chapi's stage turns a quarter turn and runs along the long
     edge anyway — the phone never has to be persuaded to rotate */
  .stage.rotate .stage-inner{
    inset:auto;left:50%;top:50%;
    transform:translate(-50%,-50%) rotate(90deg);
    transform-origin:center;
  }
  .stage-view{
    position:absolute;inset:0;
    display:flex;align-items:center;justify-content:center;
    touch-action:none;
  }
  .stage-view canvas{
    display:block;width:100%;height:100%;object-fit:contain;
  }

  /* the readout sits on the game, not beside it */
  .stage-hud{
    /* above everything, including the rotate gate: the way out is always there */
    position:absolute;left:0;right:0;top:0;z-index:6;
    display:flex;align-items:center;gap:10px;
    padding:calc(6px + env(safe-area-inset-top)) calc(10px + env(safe-area-inset-right))
            6px calc(12px + env(safe-area-inset-left));
    background:linear-gradient(rgba(0,0,0,.55), rgba(0,0,0,0));
    pointer-events:none;
  }
  .stage-name{
    font:600 10px/1 "IBM Plex Mono",monospace;letter-spacing:.12em;
    text-transform:uppercase;color:rgba(255,255,255,.55);
  }
  .stage-score{
    margin-right:auto;font:600 15px/1 "IBM Plex Mono",monospace;
    color:#fff;font-variant-numeric:tabular-nums;
  }
  .stage-x{
    pointer-events:auto;width:34px;height:34px;flex:0 0 auto;
    display:flex;align-items:center;justify-content:center;
    background:rgba(255,255,255,.12);color:#fff;
    border:0;border-radius:0;font-size:14px;line-height:1;cursor:pointer;
  }
  .stage-x:active{background:rgba(255,255,255,.28);}

  /* 2048 fills the screen and is played by swiping it */
  .stage-grid{width:min(92vw, 92vh, 460px);aspect-ratio:1;background:var(--sheet-2);border-radius:0;padding:8px;display:grid;grid-template-columns:repeat(4,1fr);gap:8px;}
  .stage-hint{
    position:absolute;left:0;right:0;bottom:14%;text-align:center;
    font:600 12px/1 "IBM Plex Mono",monospace;letter-spacing:.2em;
    color:rgba(255,255,255,.6);pointer-events:none;
    animation:hintPulse 2s ease-in-out infinite;
  }
  @keyframes hintPulse{0%,100%{opacity:.35}50%{opacity:.85}}
  @media (prefers-reduced-motion: reduce){ .stage-hint{animation:none;} }

  /* the shooter's stick: nowhere until a thumb asks for it, then under it */
  /* the shooter's control is the screen; this is only the mark under the thumb */
  .stick{
    position:absolute;z-index:3;width:52px;height:52px;
    margin:-26px 0 0 -26px;border-radius:50%;
    background:rgba(255,255,255,.16);
    border:1.5px solid rgba(255,255,255,.4);
    pointer-events:none;
  }
  .stick[hidden]{display:none;}

  /* ---- 2048 on the stage: the move is played, not cut to */
  
  /* size and motion only — the value colours come from .tile.v* and must not
     be outranked by a rule that exists to make the tiles bigger */
  .stage-grid .tile{
    font-size:clamp(18px,5.2vmin,34px);
    border-radius:0;will-change:transform;
  }
  .stage-grid .tile.merged{animation:tileThump 220ms cubic-bezier(.2,1.4,.4,1);}
  @keyframes tileThump{
    0%{transform:scale(1)} 42%{transform:scale(1.16)} 100%{transform:scale(1)}
  }
  .stage-grid.over{animation:boardSettle 420ms ease-out;}
  @keyframes boardSettle{
    0%{transform:scale(1)} 30%{transform:scale(.97)} 100%{transform:scale(1)}
  }
  @media (prefers-reduced-motion: reduce){
    .stage-grid .tile.merged,.stage-grid.over{animation:none;}
  }

  /* Chapi Bros — a wide strip of waterfront, and three pads in the corners */
  /* the field can change shape, so the canvas keeps its own ratio */
  .bros{aspect-ratio:auto;height:auto;cursor:default;}
  /* and like the other games it is not a scrolling panel — every one of them
     needs this line, and Chapi Quest was the one that never got it */
  .module[data-key^="bros"] .mod-body{max-height:none;overflow:visible;}
  .touchpads{position:absolute;inset:0;z-index:3;pointer-events:none;}

  /* the left half of the screen is the stick — it springs up wherever the thumb
     lands, so it is never somewhere the thumb has to go looking for it */
  .joy{
    position:absolute;left:0;top:0;bottom:0;width:52%;
    pointer-events:auto;touch-action:none;
  }
  .joy-base{
    position:absolute;width:96px;height:96px;margin:-48px 0 0 -48px;
    border-radius:50%;border:1.5px solid rgba(255,255,255,.34);
    background:rgba(255,255,255,.07);pointer-events:none;
  }
  .joy-base[hidden]{display:none;}
  .joy-knob{
    position:absolute;left:50%;top:50%;width:44px;height:44px;
    margin:0;border-radius:50%;transform:translate(-50%,-50%);
    background:rgba(255,255,255,.24);border:1.5px solid rgba(255,255,255,.6);
  }

  .tpad{
    position:absolute;bottom:calc(18px + env(safe-area-inset-bottom));
    border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:rgba(255,255,255,.12);border:1.5px solid rgba(255,255,255,.36);
    color:rgba(255,255,255,.8);line-height:1;
    pointer-events:auto;touch-action:none;cursor:pointer;
  }
  .tpad.on{background:rgba(255,255,255,.32);transform:scale(.94);}
  .tpad.jump{right:calc(22px + env(safe-area-inset-right));width:96px;height:96px;font-size:26px;}

  /* the launcher's still of the game it opens */
  .thumb{
    display:block;width:100%;height:auto;aspect-ratio:32/15;
    border-radius:0;background:var(--sheet);
  }
  .fs-best{
    margin-left:auto;font-weight:600;letter-spacing:.04em;
    color:rgba(255,255,255,.7);text-transform:none;
  }

  /* 2048's ending: the score runs up to itself rather than simply appearing */
  .game-done, .stage-done{
    display:flex;flex-direction:column;align-items:center;gap:7px;
    padding:14px 0 4px;text-align:center;
  }
  .game-done-cap, .stage-done-cap{
    font:600 10px/1 "IBM Plex Mono",monospace;letter-spacing:.16em;
    text-transform:uppercase;color:var(--ink-3);
  }
  .game-done-score, .stage-done-score{
    font:700 30px/1 "IBM Plex Mono",monospace;color:var(--ink);
    font-variant-numeric:tabular-nums;
    transition:transform 180ms cubic-bezier(.2,1.4,.4,1);
  }
  .game-done-score.landed, .stage-done-score.landed{transform:scale(1.08);}
  .stage-done{
    position:absolute;inset:0;z-index:4;justify-content:center;gap:12px;
    background:rgba(8,9,12,.72);
  }
  .stage-done-score{font-size:clamp(34px,11vmin,68px);color:#fff;}
  .stage-done .btn{pointer-events:auto;padding:9px 16px;}

  /* ---- spells: a board folded into a word ---- */
  .main-panel{min-width:230px;max-width:min(320px,90vw);padding:5px;gap:0;
    max-height:min(70vh,560px);overflow-y:auto;}
  .main-panel .spell-head:first-of-type{margin-top:4px;}
  .menu-link{display:block;padding:8px 9px;font-size:12.5px;color:var(--ink);
    text-decoration:none;border-radius:0;}
  .menu-link:hover{background:var(--sheet-2);}
  .menu-back{text-align:left;background:none;border:none;padding:6px 9px;font-size:11px;
    font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--ink-3);
    cursor:pointer;border-radius:0;}
  .menu-back:hover{color:var(--accent);}
  
  .spell-head{font-size:10px;font-weight:700;letter-spacing:0.09em;text-transform:uppercase;
    color:var(--ink-3);margin:14px 0 7px;}
  .spell-head:first-child{margin-top:0;}
  .spell-word{font-family:'IBM Plex Mono',monospace;font-size:11.5px;line-height:1.7;
    color:var(--ink);background:var(--wash);border-radius:0;padding:8px 9px;
    word-break:break-all;user-select:all;}
  .spell-row{display:flex;gap:6px;margin-top:8px;align-items:center;}
  .spell-row .btn{flex:0 0 auto;text-decoration:none;line-height:1.4;}
  .spell-row input{flex:1 1 auto;min-width:0;font-family:'IBM Plex Mono',monospace;font-size:11.5px;}
  .spell-note{font-size:11.5px;line-height:1.6;color:var(--ink-3);margin:10px 0 0;}
  .spell-err{font-size:11.5px;color:var(--neg);margin-top:7px;}
  .spell-err[hidden]{display:none;}
  .path-item.on b{color:var(--accent);}
  .path-item.on small{color:var(--ink-2);}

  /* ---- the dealing desk ---- */
  .mkt-row .row-sub{font-size:10.5px;color:var(--ink-3);}
  .module.wide .mkt-row{border-top:1px solid var(--line);}

  .tape{display:flex;overflow:hidden;mask-image:linear-gradient(90deg,transparent,#000 3%,#000 97%,transparent);
    -webkit-mask-image:linear-gradient(90deg,transparent,#000 3%,#000 97%,transparent);}
  .tape-run{display:flex;flex:0 0 auto;gap:22px;padding-right:22px;animation:tape-run 90s linear infinite;}
  .tape-item{display:flex;align-items:baseline;gap:7px;font-size:12px;white-space:nowrap;}
  .tape-item b{font-weight:600;color:var(--ink);letter-spacing:0.02em;}
  .tape-item .mono{font-size:11.5px;color:var(--ink-2);}
  .tape-item .up{color:var(--pos);}
  .tape-item .down{color:var(--neg);}
  @keyframes tape-run{from{transform:translateX(0);}to{transform:translateX(-100%);}}
  @media (prefers-reduced-motion: reduce){
    .tape{overflow-x:auto;mask-image:none;-webkit-mask-image:none;}
    .tape-run{animation:none;}
    .tape-run[aria-hidden]{display:none;}
  }

  .curve{width:100%;height:auto;display:block;margin:2px 0 8px;}
  .curve-lab{font-family:'IBM Plex Mono',monospace;font-size:8px;fill:var(--ink-3);}
  .curve-val{font-family:'IBM Plex Mono',monospace;font-size:8.5px;font-weight:600;fill:var(--ink-2);}
  .price.up{color:var(--pos);}
  .price.down{color:var(--neg);}

  .xr-wrap{overflow-x:auto;}
  .xr{border-collapse:collapse;width:100%;font-size:11.5px;font-variant-numeric:tabular-nums;}
  .xr th,.xr td{padding:5px 7px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap;}
  .xr thead th{font-size:10px;font-weight:700;letter-spacing:0.06em;color:var(--ink-3);}
  .xr th.xr-row{text-align:left;font-weight:600;color:var(--ink);}
  .xr td{color:var(--ink-2);}
  .xr .xr-self{color:var(--ink-3);}
  .xr tbody tr:hover td{background:var(--sheet-2);}

  .sess-row{align-items:flex-start;}
  .sess-dot{width:7px;height:7px;border-radius:50%;background:var(--ink-3);opacity:0.45;margin-top:6px;flex:0 0 auto;}
  .sess-dot.on{background:var(--pos);opacity:1;box-shadow:0 0 0 3px color-mix(in srgb,var(--pos) 22%,transparent);}
  .sess-row .row-main{min-width:0;}
  .sess-bar{height:2px;background:var(--line);border-radius:0;margin-top:6px;overflow:hidden;}
  .sess-bar span{display:block;height:100%;background:var(--accent);}

  .lot-form input[name="qty"],.lot-form input[name="cost"]{max-width:74px;}
  .pos-total{border-top:2px solid var(--ink-3);}
  .pos-total .row-title{font-weight:700;}

  /* movers: the size of the move reads before the number does */
  .mover-row{position:relative;overflow:hidden;}
  .mover-bar{position:absolute;left:0;top:4px;bottom:4px;border-radius:0;opacity:0.13;z-index:0;}
  .mover-bar.up{background:var(--pos);}
  .mover-bar.down{background:var(--neg);}
  .mover-row > *:not(.mover-bar){position:relative;z-index:1;}
  .mover-row .row-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

  /* the wire: every headline arrives already attached to a ticker */
  .wire-item{display:flex;flex-direction:column;gap:5px;}
  .wire-tags{display:flex;flex-wrap:wrap;gap:5px;}
  .wire-tag{font-size:10px;font-weight:700;letter-spacing:0.04em;color:var(--accent);
    background:var(--wash);border-radius:0;padding:3px 5px;font-variant-numeric:tabular-nums;}
  .wire-tag .up{color:var(--pos);font-weight:600;}
  .wire-tag .down{color:var(--neg);font-weight:600;}

  /* ---- the chart: candles for one name ---- */
  .chart-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;}
  /* the name is the control: it looks like the label it replaces until you
     put a pointer on it */
  .chart-name{display:block;text-align:left;background:none;border:0;padding:0;margin:0;
    color:inherit;cursor:pointer;border-bottom:1px solid transparent;}
  .chart-name:hover{border-bottom-color:var(--accent);}
  .chart-name:hover b{color:var(--accent);}
  .chart-name b{font-family:'IBM Plex Mono',monospace;font-size:14px;font-weight:600;display:block;}
  .chart-name-form{margin:6px 0 4px;}
  .chart-name .row-sub{display:block;}
  .chart-last{text-align:right;}
  .chart-last .price{font-size:16px;}
  /* the line of numbers under the head, and the crosshair that picks them */
  .chart-read{display:flex;flex-wrap:wrap;gap:2px 12px;font-size:11px;color:var(--ink-3);
    padding:2px 0 4px;min-height:16px;}
  .chart-read b{font-weight:600;color:var(--ink);}
  .chart-read .read-when{color:var(--ink-2);font-weight:600;}
  .chart-read .up{color:var(--pos);}
  .chart-read .down{color:var(--neg);}
  /* a finger reading the chart should not also scroll the page sideways */
  .candles-box{width:100%;height:210px;touch-action:pan-y;cursor:crosshair;overflow:hidden;}
  .module.wide .candles-box{height:270px;}
  .module.tall .candles-box{height:320px;}
  .module.tall.wide .candles-box{height:430px;}
  .board-grid.zoomed .candles-box{height:58vh;}
  .candles{display:block;width:100%;height:100%;overflow:visible;}

  /* ---- card perks: a rebate is only worth the fee if it is spent ---- */
  .perk-tally{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
    padding-bottom:6px;font-size:12.5px;color:var(--ink-3);}
  .perk-tally b{font-size:17px;font-weight:600;color:var(--ink);}
  .perk-card{display:flex;flex-direction:column;}
  .perk-name{display:flex;align-items:center;justify-content:space-between;
    margin-top:10px;color:var(--ink);}
  .perk-row{gap:9px;}
  .perk-row input[type="checkbox"]{flex:none;width:15px;height:15px;accent-color:var(--accent);
    cursor:pointer;}
  /* the cadence is the part you scan for: how often this comes back */
  .perk-every{color:var(--ink-2);font-weight:600;}
  .perk-amount{width:70px;text-align:right;font-size:13px;padding:3px 6px;border-radius:0;
    border:1px solid var(--line);background:var(--sheet-2);color:var(--ink);}
  .perk-row.done .row-title{color:var(--ink-3);text-decoration:line-through;}
  .perk-row.done .price{color:var(--ink-3);}

  /* ---- bookmarks ---- */
  .link-title{font-weight:600;font-size:13px;color:var(--ink);text-decoration:none;}
  .link-title:hover{color:var(--accent);text-decoration:underline;}
  .link-row .row-sub{font-size:11px;}

  /* ---- jiggle mode: rearranging the board the way a home screen does ---- */
  /* a tremor, not a shake: enough to say "these can move", no more */
  @keyframes jiggle{
    0%{transform:rotate(-0.15deg);}
    50%{transform:rotate(0.15deg);}
    100%{transform:rotate(-0.15deg);}
  }
  /* the page still scrolls while rearranging; only a title bar is picked up */
  .jiggling .module{animation:jiggle 0.32s ease-in-out infinite;touch-action:pan-y;transform-origin:50% 50%;animation-duration:0.5s;}
  /* out of step with each other, or the board looks like one shivering sheet */
  .jiggling .module:nth-child(2n){animation-delay:-0.11s;animation-duration:0.46s;}
  .jiggling .module:nth-child(3n){animation-delay:-0.19s;animation-duration:0.55s;}
  .jiggling .module:nth-child(5n){animation-delay:-0.05s;animation-duration:0.42s;}
  .jiggling .module.dragging,.drag-ghost{animation:none;}
  .jiggling .mod-head{cursor:grabbing;touch-action:none;}
  /* nothing inside a widget takes a tap while the board is being rearranged */
  .jiggling .mod-body{pointer-events:none;}
  .jiggling .mod-actions{pointer-events:auto;}
  .jiggling .icon-btn{opacity:0.75;}

  .jiggle-bar[hidden]{display:none;}

  @media (prefers-reduced-motion: reduce){
    .jiggling .module{animation:none;outline:1px dashed var(--accent);outline-offset:3px;}
  }

  /* ---- what's on: a listing laid out as an event, not a headline ---- */
  .event-shot{flex:0 0 46px;width:46px;height:46px;object-fit:cover;border-radius:0;
    background:var(--sheet-2);align-self:center;}
  /* a listing with a picture, a date and a title needs more than a column of a
     news feed to be read in */
  .module.wide.t-events .mod-body{grid-template-columns:repeat(auto-fill,minmax(330px,1fr));}
  .event-row{display:flex;align-items:baseline;gap:12px;padding:9px 0;
    border-top:1px solid var(--line);text-decoration:none;color:var(--ink);}
  .event-row:first-child{border-top:none;}
  .event-row:hover .event-title{color:var(--accent);}
  .event-when{flex:0 0 64px;display:flex;flex-direction:column;gap:2px;white-space:nowrap;}
  .event-day{font-size:11.5px;font-weight:600;color:var(--ink);}
  .event-time{font-size:10.5px;color:var(--ink-3);}
  .event-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px;}
  .event-title{font-size:13px;line-height:1.35;}
  .event-where{font-size:11px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .event-cost{flex:0 0 auto;font-size:11px;color:var(--ink-2);}
  .event-cost.free{color:var(--pos);font-weight:600;}

  /* countdowns: the number of days is the whole point, so it is the whole size */
  .cd-value{display:flex;flex-direction:column;align-items:flex-end;gap:0;}
  .cd-days{font-size:34px;font-weight:600;line-height:1;letter-spacing:-0.03em;
    font-variant-numeric:tabular-nums;color:var(--ink);}
  .cd-days.past{color:var(--ink-3);}
  .cd-value .row-sub{font-size:10.5px;margin-top:3px;}
  .cd-row{padding:11px 0;}
  .board-grid.zoomed .cd-days{font-size:52px;}

  /* ---- deals: the price is the headline, the name is the reason ---- */
  .deal-row{display:flex;align-items:flex-start;gap:12px;padding:10px 0;
    border-top:1px solid var(--line);text-decoration:none;color:var(--ink);}
  .wk-tabs + .deal-row{border-top:none;}
  .deal-row:hover .deal-title{color:var(--accent);}
  .deal-shot{flex:0 0 52px;width:52px;height:52px;object-fit:contain;border-radius:0;
    background:#fff;}
  .deal-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:4px;}
  .deal-title{font-size:13px;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;
    -webkit-box-orient:vertical;overflow:hidden;}
  .deal-meta{font-size:11px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .deal-badges{display:flex;flex-wrap:wrap;gap:5px;}
  .deal-off,.deal-chip,.deal-score{font:600 10px/1 'IBM Plex Mono',monospace;padding:3px 5px;
    border-radius:0;letter-spacing:0.02em;}
  .deal-off{background:var(--neg);color:#fff;}
  .deal-chip{background:var(--wash);color:var(--ink-2);}
  .deal-score{color:var(--ink-3);border:1px solid var(--line);}
  .deal-score.hot{color:var(--pos);border-color:color-mix(in srgb,var(--pos) 45%,transparent);}
  .deal-price{flex:0 0 auto;display:flex;flex-direction:column;align-items:flex-end;gap:2px;
    text-align:right;min-width:52px;}
  .deal-now{font-size:16px;font-weight:600;letter-spacing:-0.01em;font-variant-numeric:tabular-nums;}
  .deal-was{font-size:11px;color:var(--ink-3);text-decoration:line-through;}
  /* a shop's standing offer: true all week, so it reads as a note, not a listing */
  .deal-row.promo{background:var(--wash);border-top:none;border-radius:0;padding:10px 12px;
    margin:6px 0 4px;}
  .deal-row.promo .deal-title{font-weight:600;}
  .deal-promo-tag{font:700 10px/1 'IBM Plex Mono',monospace;letter-spacing:0.08em;
    text-transform:uppercase;color:var(--accent);}

  /* ---- the city: said after the board's name, and the way to move it ---- */
  .mark-city{display:inline-flex;align-items:baseline;gap:0.28em;margin-left:0.45em;
    font:inherit;font-family:'IBM Plex Mono',monospace;font-weight:500;
    font-size:0.52em;letter-spacing:0;background:none;border:0;padding:0;
    color:var(--ink-2);cursor:pointer;border-bottom:2px solid transparent;
    white-space:nowrap;align-self:baseline;}
  .mark-city:hover,.mark-city[aria-expanded="true"]{color:var(--accent);border-bottom-color:var(--accent);}
  .mark-city:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  .city-panel{margin-top:2px;margin-bottom:24px;padding-top:14px;border-top:2px solid var(--ink);
    display:flex;flex-direction:column;gap:12px;}
  .city-panel[hidden]{display:none;}
  .city-search{display:flex;max-width:420px;}
  .city-search .combo{flex:1;}
  .city-picks{display:flex;flex-wrap:wrap;gap:5px;}
  .city-picks .chip{font-size:12px;padding:5px 9px;}
  .city-note{margin:0;font-size:12px;line-height:1.55;color:var(--ink-3);max-width:62ch;}
  @media (max-width: 640px){
    
    .city-panel{margin-bottom:18px;}
  }
  /* On a phone the title and the city do not fit beside the menu on one line.
     The city drops under the board's name, and the menu keeps its corner. */
  .topbar .wordmark{flex-wrap:wrap;min-width:0;row-gap:6px;}
  .topbar .add-menu{flex:0 0 auto;align-self:flex-start;}
  @media (max-width: 640px){
    .topbar{flex-wrap:nowrap;}

  }

  /* On a phone the dial took the whole width of the screen and the numbers
     under it spilled into the next widget. A phone is held close: a dial a
     third of the width reads fine, and the rest of the board stays in view. */
  @media (max-width: 640px){
    .today .face.today-face{margin-bottom:10px;}

    .today-facts{margin-top:12px;gap:5px;font-size:12.5px;}
    .module.t-today .mod-body{height:auto;}
  }

  /* the rest of a list, one press away */
  .more-toggle{display:block;width:100%;margin-top:6px;padding:9px 0;background:none;border:0;
    border-top:1px solid var(--line);font:600 12px/1 'Archivo',system-ui,sans-serif;
    color:var(--accent);cursor:pointer;text-align:left;}
  .more-toggle:hover{text-decoration:underline;}
  .more-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  /* ---- a widget's corner says one thing: open it up ----
     Width, height, the pencil and remove are editing, and editing is a mode:
     "Edit the board" in the menu, or holding a title on a phone. Outside it,
     each widget offers only the way to see its contents full screen. */
  .mod-actions .icon-btn:not([data-zoom]){display:none;}
  .jiggling .mod-actions .icon-btn,
  .module.editing .mod-actions .icon-btn{display:flex;}
  /* on a touch screen there is no hover to reveal it, so it is simply there */
  @media (hover: none){ .mod-actions .icon-btn[data-zoom]{opacity:0.8;} }
  /* editing a widget's own list still works while the board is in edit mode */
  .jiggling .module.editing .mod-body{pointer-events:auto;}
  /* a desk does not need the phone's wobble to know it is editing */
  @media (hover: hover){
    .jiggling .module{animation:none;outline:1px dashed color-mix(in srgb,var(--accent) 55%,transparent);
      outline-offset:6px;}
  }

  /* ---- full screen means the whole screen ---- */
  body.zoom-open{overflow:hidden;}
  body.zoom-open .topbar,
  body.zoom-open .path-index,
  body.zoom-open .city-panel,
  body.zoom-open .notice,
  body.zoom-open footer,
  body.zoom-open .jiggle-bar{display:none;}
  body.zoom-open .board-grid.zoomed .module{
    position:fixed;inset:0;z-index:300;margin:0;min-height:0;max-width:none;
    background:var(--paper);overflow-y:auto;overscroll-behavior:contain;
    padding:0 calc(18px + env(safe-area-inset-right))
            calc(28px + env(safe-area-inset-bottom)) calc(18px + env(safe-area-inset-left));
    animation:none;border-top:none;}
  body.zoom-open .board-grid.zoomed .mod-actions .icon-btn[data-zoom]{opacity:1;width:34px;height:34px;}
  body.zoom-open .board-grid.zoomed .mod-body{max-width:1100px;margin:0 auto;width:100%;}

  /* ---- News: a subject at a time, and pictures that lead ---- */
  
  .news-hero{flex-direction:column;gap:8px;border-top:none;padding-top:2px;}
  .news-hero-shot{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:0;background:var(--sheet-2);}
  .news-hero .news-title{font-size:15px;font-weight:600;line-height:1.35;}
  
  .wk-tabs + .news-item{border-top:none;}

  /* ---- phones ---- */
  @media (max-width: 640px){
    /* A widget that scrolls inside a page that scrolls makes the thumb pick one.
       On a phone the page wins: widgets grow to fit, and long lists stop at a
       few with Show more under them. */
    .mod-body{max-height:none;overflow:visible;}
    .wk-tabs{overflow-x:auto;scrollbar-width:none;flex-wrap:nowrap;}
    .wk-tabs::-webkit-scrollbar{display:none;}

    /* the world clock: two cities a row, each name on a line of its own */
    .clock-line{display:grid;grid-template-columns:1fr 1fr;}
    .clock-seg{min-width:0;text-align:left;padding:10px 12px;border-left:none;border-top:1px solid var(--line);}
    .clock-seg:nth-child(2n){border-left:1px solid var(--line);}
    .clock-seg:nth-child(-n+2){border-top:none;}
    .clock-seg .seg-city{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:12.5px;}
    .clock-seg .seg-day{display:block;margin:2px 0 0;}
    .clock-seg .seg-time,.module.wide .clock-seg .seg-time{font-size:24px;margin-top:4px;}
  }
  @media (max-width: 640px){
    /* analog faces: the dial beside the name, not a dot above it */
    .clock-line.faces .clock-seg{display:grid;grid-template-columns:46px 1fr;column-gap:10px;
      align-items:center;padding:10px 12px;}
    .clock-line.faces .face{width:46px;height:46px;margin:0;grid-row:1 / span 2;}
    .clock-line.faces .seg-city{align-self:center;}
  }
  @media (max-width: 640px){
    /* the tall and wide variants set their own ceilings; on a phone none apply */
    .module .mod-body,
    .module.tall .mod-body,
    .module.tall.wide .mod-body,
    .module.wide .mod-body{max-height:none;min-height:0;overflow:visible;}
  }

  /* The city stays on the title's line. On a narrow screen the type steps down
     and a long city name gives way with an ellipsis rather than a line of its
     own; the board's name is never the part that is cut. */
  @media (max-width: 640px){
    .topbar{align-items:center;}
    .topbar .wordmark{flex-wrap:nowrap;min-width:0;overflow:hidden;font-size:23px;}
    .topbar .wordmark .mark-name,.topbar .wordmark .mark-path{flex:0 0 auto;}
    .mark-city{flex:0 1 auto;flex-basis:auto;min-width:0;margin-left:0.4em;font-size:13px;
      overflow:hidden;}
    .mark-city [data-mark-city]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
  }

  /* ---- tabs in the title line: the name on the left, the choices on the right ---- */

  /* right-aligned when there is room; scrollable from the first tab when there is not */

  /* The title shrinks and its description ends in an ellipsis; only when the
     widget has tabs (and so no description) does it hold its width. A title
     that could not shrink pushed some boards out to 480px wide on a phone. */
  .mod-head .mod-title{flex:0 1 auto;min-width:0;}

  /* The city is no longer part of the title: it sits at the right of the bar,
     beside the menu, with no "in" in front of it. */
  .topbar > .mark-city{margin-left:0;font-size:14px;align-self:center;flex:0 1 auto;min-width:0;
    max-width:40vw;overflow:hidden;}
  .topbar > .mark-city [data-mark-city]{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  @media (max-width: 640px){
    .topbar .spacer{display:block;flex:1 1 auto;min-width:8px;}
    .topbar .wordmark{flex:0 1 auto;}
    .topbar > .mark-city{font-size:12.5px;max-width:34vw;}
  }

  /* ---- one tab design for every widget ----
     Square, quiet until chosen, and in the title line: News and its subjects,
     Deals and its sort, Movers, the weekend dates, the chart's ranges. */

  /* any tab bar still inside a widget's body wears the same clothes */
  .mod-body .wk-tabs{background:none;border:none;border-radius:0;gap:2px;}

  /* full screen has the room: the choices get a line of their own under the name */
  body.zoom-open .board-grid.zoomed .mod-head{flex-wrap:wrap;row-gap:10px;}

  /* Nothing on a board may make the page wider than the screen. clip, not
     hidden: hidden turns the body into a scroll container of its own, which
     is its own source of trouble on iOS. */
  html,body{overflow-x:clip;}
  .module,.mod-head,.mod-body{min-width:0;}

  /* ---- Today on a phone: one line, not a wall clock ----
     A dial the width of the screen says nothing a glance at the status bar does
     not. On a phone it folds to a line: a small dial, the time, the date. The
     year and the count of days stay on the desk-sized board. */
  @media (max-width: 640px){
    .module.t-today .mod-body{container-type:normal;}
    .today{padding:2px 0;}
    .today-main{flex-direction:row;align-items:center;flex-wrap:nowrap;min-width:0;}
    .today .face.today-face{flex:0 0 40px;min-height:0;margin:0;}
    .today-time{letter-spacing:-0.02em;flex:0 0 auto;}
    .today-date{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;}
    .today-year,.today-facts{display:none;}
  }

  /* the edit bar lives in the page, under the title, and stays in view as the
     page scrolls; a bar pinned to the bottom of an iPhone sits under Safari's
     own toolbar, where it could not be found */
  .jiggle-bar{position:sticky;top:0;left:auto;bottom:auto;transform:none;z-index:210;
    display:flex;align-items:center;justify-content:space-between;gap:12px;
    margin:0 0 16px;padding:calc(9px + env(safe-area-inset-top)) 12px 9px;border-radius:0;
    background:var(--wash);border:none;border-bottom:2px solid var(--accent);box-shadow:none;
    font-size:12.5px;line-height:1.4;color:var(--ink-2);white-space:normal;}
  .jiggle-bar b{color:var(--ink);}
  .jiggle-bar .btn{border-radius:0;padding:7px 14px;flex:0 0 auto;}

  /* ---- the info button and what it opens ---- */
  
  @media (hover: none){ .mod-actions .icon-btn[data-info]{opacity:0.8;} }
  .icon-btn.info-btn.on{color:var(--accent);opacity:1;}

  /* ---- a widget waiting on setup: greyed, with the way to do it ---- */
  .needs-setup{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:12px;
    min-height:140px;padding:18px 16px;
    background:repeating-linear-gradient(135deg,var(--sheet-2) 0 10px,transparent 10px 20px);
    border:1px dashed var(--line);color:var(--ink-3);}
  .needs-setup-words{display:flex;flex-direction:column;gap:4px;max-width:46ch;}
  .needs-setup-words b{color:var(--ink-2);font-size:13.5px;}
  .needs-setup-words span{font-size:12.5px;line-height:1.5;}
  .needs-setup .btn{border-radius:0;text-decoration:none;}
  .module:has(.needs-setup) .mod-label{color:var(--ink-3);}

  /* the cheapest weekend trip says so */
  .cheapest-badge{margin-left:8px;vertical-align:middle;}

  /* =========================================================================
     The widget component. Every widget's header is the same three slots, so
     spacing cannot drift from one widget to the next:
       title | what it is, or its tabs | actions
     On full screen it becomes an app bar: back, title, actions, and the tabs on
     a line of their own beneath.
     ========================================================================= */

  .mod-tabs{display:flex;gap:2px;min-width:0;max-width:100%;overflow-x:auto;overflow-y:hidden;
    scrollbar-width:none;touch-action:pan-x pan-y;-webkit-overflow-scrolling:touch;}
  .mod-tabs::-webkit-scrollbar{display:none;}
  .mod-tab{flex:0 0 auto;border:0;border-radius:0;background:transparent;color:var(--ink-3);cursor:pointer;font:600 11px/1 'Archivo',system-ui,sans-serif;letter-spacing:0.01em;padding:6px 9px;white-space:nowrap;line-height:16px;height:28px;box-sizing:border-box;display:inline-flex;align-items:center;padding-top:0;padding-bottom:0;}
  .mod-tab:hover{color:var(--ink);background:var(--sheet-2);}
  .mod-tab.on{background:var(--ink);color:var(--paper);}
  .mod-tab .mod-tab-note{margin-left:6px;font-family:'IBM Plex Mono',monospace;font-weight:500;opacity:0.75;}
  .mod-tab:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}

  /* the title as a switch between the kinds of its family */
  .mod-kind{min-width:0;position:relative;display:inline-flex;align-items:center;gap:6px;cursor:pointer;}
  .mod-kind select{appearance:none;-webkit-appearance:none;border:0;border-radius:0;background:transparent;color:var(--ink);font:700 11px/1 'Archivo',system-ui,sans-serif;text-transform:uppercase;letter-spacing:0.14em;padding:6px 20px 6px 0;max-width:100%;padding-top:0;padding-bottom:0;position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;font-size:16px;}
  .mod-kind::after{position:absolute;right:4px;top:50%;width:6px;height:6px;margin-top:-5px;border-right:1.5px solid var(--ink-3);border-bottom:1.5px solid var(--ink-3);transform:rotate(45deg);pointer-events:none;content:none;}
  .mod-kind select:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

  /* full screen: the widget as an app.
     The app fills the screen, notch and all, so something has to hold its
     header clear of the notch — but only one thing may. The scroller used to
     inset its top too, and a sticky header cannot rise above its scroller's
     padding, so the inset was spent twice over: on a notched phone the title
     sat 136px down behind an empty band, and even without a notch the header
     was 18px top against 10px bottom. The scroller's top padding is now 0 and
     this header carries the whole of it, which is also why it reads the same
     stuck as at rest, and why its own background is what fills the notch. */
  body.zoom-open .board-grid.zoomed .module.is-app .mod-head{grid-template-columns:auto minmax(0,1fr) auto;
    position:sticky;top:0;z-index:3;background:var(--paper);border-bottom:1px solid var(--line);
    margin:0 0 12px;padding:calc(10px + env(safe-area-inset-top)) 0 10px;}
  body.zoom-open .module.is-app .mod-head > .app-back{grid-column:1;grid-row:1;width:36px;height:36px;opacity:1;}
  body.zoom-open .module.is-app .mod-head > .mod-title{grid-column:2;grid-row:1;}
  body.zoom-open .module.is-app .mod-head > .mod-actions{grid-column:3;grid-row:1;}
  body.zoom-open .module.is-app .mod-head > .mod-sub{grid-column:1 / -1;grid-row:2;}
  body.zoom-open .module.is-app .mod-head > .mod-sub:empty{display:none;}
  body.zoom-open .module.is-app .mod-label,
  body.zoom-open .module.is-app .mod-kind select{font-size:13px;}
  body.zoom-open .module.is-app .mod-tab{font-size:12.5px;padding:8px 12px;height:34px;}
  body.zoom-open .module.is-app .mod-actions .icon-btn{width:34px;height:34px;opacity:1;}
  body.zoom-open .module.is-app .zoom-btn{display:none;}          /* the back arrow is the way out */
  .app-tools{margin:0 0 12px;}
  /* an app shows its add and remove controls at full strength */
  body.zoom-open .module.is-app .row-remove{opacity:1;}

  /* in the corner, outside edit mode: what it is, and open it */
  .mod-actions .icon-btn[data-zoom],.mod-actions .icon-btn[data-info]{display:flex;}
  /* a header is at least this tall whether or not it has a second row */

  .mod-kind .mod-caret{width:6px;height:6px;margin-top:-4px;border-right:1.5px solid var(--ink-3);
    border-bottom:1.5px solid var(--ink-3);transform:rotate(45deg);flex:0 0 auto;}
  .mod-kind:hover .mod-label,.mod-kind:hover .mod-caret{color:var(--accent);border-color:var(--accent);}
     /* 16px so iOS does not zoom when it opens */
  .mod-kind:focus-within{outline:2px solid var(--accent);outline-offset:3px;}

  /* an app's body reads from the top; the board's fill-the-row layout does not apply */
  body.zoom-open .module.is-app{display:block;}
  body.zoom-open .module.is-app .mod-body{display:flex;flex-direction:column;justify-content:flex-start;align-content:flex-start;gap:10px;min-height:0;height:auto;flex:none;align-items:stretch;}
  body.zoom-open .module.is-app .chips{justify-content:flex-start;}
  @media (max-width: 640px){
    /* the one-line clock sits in the middle of its widget, as the big one did */
    .today-main{justify-content:center;text-align:center;}
  }

  /* ---- a widget's contents stay inside the widget ---- */
  .row-remove{margin-right:0;}

  .today-date .date-short{display:none;}
  @media (max-width: 640px){
    .today-date .date-long{display:none;}
    .today-date .date-short{display:inline;}
    .today-main{gap:10px;}
    .today .face.today-face{flex-basis:36px;width:36px;height:36px;}
    .today-time{font-size:22px;}
  }
  
  body.zoom-open .module.is-app .mod-body > *{align-self:stretch;max-width:100%;}
  /* the clock as an app is the one place a big clock is the point */
  body.zoom-open .module.is-app .today-main{flex-direction:column;justify-content:center;gap:8px;min-height:60vh;}
  body.zoom-open .module.is-app .today .face.today-face{width:min(260px,62vw);height:auto;flex:0 0 auto;min-height:0;}
  body.zoom-open .module.is-app .today-time{font-size:48px;}
  body.zoom-open .module.is-app .today-date .date-long{display:inline;}
  body.zoom-open .module.is-app .today-date .date-short{display:none;}
  body.zoom-open .module.is-app .today-year,
  body.zoom-open .module.is-app .today-facts{display:flex;}
  body.zoom-open .module.is-app .today-year{display:block;}

  /* ---- what a widget is: a tooltip on the ⓘ beside its name ----
     It hangs from the header, square like everything else on the board, and
     never moves the widget's contents. With a mouse it opens on hover — it
     waits a moment before closing, so the pointer can cross onto it; on a
     phone a tap pins it open and a tap anywhere else puts it away. */

  .mod-title .info-btn{width:20px;height:20px;margin-left:-2px;opacity:0.5;color:var(--ink-3);}
  .module:hover .mod-title .info-btn{opacity:0.8;}
  .mod-title .info-btn:hover,.mod-title .info-btn:focus-visible,.mod-title .info-btn.on{opacity:1;color:var(--ink);background:none;}
  @media (hover: none){ .mod-title .info-btn{opacity:0.7;} }
  .mod-info{position:absolute;z-index:120;top:calc(100% + 2px);left:0;right:auto;width:min(300px,100%);
    margin:0;background:var(--ink);color:var(--paper);border:0;border-radius:0;
    padding:10px 12px 4px;box-shadow:0 8px 24px rgba(0,0,0,0.22);
    font-size:12px;line-height:1.45;text-align:left;text-transform:none;letter-spacing:normal;font-weight:400;
    visibility:hidden;opacity:0;transition:opacity .12s ease, visibility 0s linear .18s;}
  /* the gap between the icon and the note still counts as the note */
  .mod-info::before{content:"";position:absolute;left:0;right:0;top:-10px;height:10px;}
  .mod-info.on,
  .mod-head:has(.info-btn:focus-visible) > .mod-info{visibility:visible;opacity:1;transition-delay:0s;}
  @media (hover: hover){
    .mod-head:has(.info-btn:hover) > .mod-info,
    .mod-info:hover{visibility:visible;opacity:1;transition-delay:0s;}
  }
  .mod-info p{margin:0 0 7px;font-size:12px;line-height:1.45;color:inherit;}
  .mod-info .mod-info-from,.mod-info .mod-info-needs{color:inherit;opacity:0.72;font-size:11.5px;}
  .mod-info .mod-info-needs b{color:inherit;}
  .mod-info a{color:inherit;text-decoration:underline;}
  body.zoom-open .module.is-app .mod-title .info-btn{width:28px;height:28px;opacity:0.8;}

  /* ---- one countdown is a date, not a list ----
     A single countdown drew one thin row and left the rest of the card empty.
     With nothing to line up against, the number is the widget. */
  .cd-solo{display:flex;flex-direction:column;align-items:center;justify-content:center;
    flex:1;min-height:0;gap:2px;padding:6px 0 10px;text-align:center;}
  .cd-solo-days{font-family:'IBM Plex Mono',monospace;font-weight:600;line-height:0.92;
    font-size:clamp(56px,22cqw,150px);letter-spacing:-0.03em;}
  .cd-solo-days.past{color:var(--ink-3);}
  .cd-solo-label{font-size:12.5px;color:var(--ink-2);letter-spacing:0.02em;}
  .cd-solo-name{margin-top:10px;font-weight:600;font-size:15px;}
  .cd-solo-date{font-size:12px;color:var(--ink-2);}

  /* ---- a world clock fills the card it is given ----
     Three cities in a tall card huddled at the top and left the rest blank.
     The rows share out the height instead; fitClocks then grows the digits
     into whatever slack is left, the same way the Today clock does. */
  .t-clock .mod-body{display:flex;flex-direction:column;}
  /* The rows keep their natural height on purpose: stretching them to fill the
     card would spend the very slack fitClocks measures, and three cities spaced
     a hand apart reads worse than three cities told in big digits. */

  /* the theme in the menu: a word and a dropdown, the same height as the rows around it */
  .menu-select{display:flex;align-items:center;justify-content:space-between;gap:12px;
    padding:4px 4px 4px 9px;font-size:12.5px;color:var(--ink);}
  .menu-select select{appearance:none;-webkit-appearance:none;border:1px solid var(--line);background:var(--paper)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23969ba5' stroke-width='1.5'/%3E%3C/svg%3E")
    no-repeat right 9px center;color:var(--ink);font:500 12.5px/1.2 'Archivo',system-ui,sans-serif;
    padding:6px 28px 6px 9px;min-width:104px;cursor:pointer;}
  .menu-select select:hover{border-color:var(--ink-3);}
  .menu-select select:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
  .jiggle-bar .icon-done{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;}

  /* Today's clock already opted out of the 300px cap so it could fill its card.
     A world clock and a countdown have the same problem and now the same answer:
     take the height the row actually gives, and scroll only if the contents
     genuinely exceed it. */
  .module.t-clock .mod-body,
  .module.t-countdown .mod-body{flex:1;min-height:0;max-height:none;overflow-y:auto;}
  .add-panel button.on{color:var(--ink);background:var(--sheet-2);}

  /* No rounded corners anywhere on the board — not on a card, a chip, a button,
     a field or a note. Circles stay circles (a dial, a dot); corners are square.
     Form controls are named because browsers round them on their own. */
  button,input,select,textarea{border-radius:0;}

  /* ---- News as a slideshow ----
     The picture takes the widget, the headline sits under it, and the slides
     are stacked in one place so the next one fades in over the last. */
  .news-foot{grid-column:1/-1;display:flex;align-items:stretch;gap:16px;border-top:1px solid var(--line);margin-top:6px;}
  .news-foot .more-toggle{width:auto;margin:0;border-top:0;flex:0 0 auto;}
  .news-foot .news-view{margin-left:auto;color:var(--ink-3);}
  .news-foot .news-view:hover{color:var(--ink);}
  .news-slides{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;min-width:0;flex:1;}
  .news-slide-stage{position:relative;display:grid;min-height:0;}
  .news-slide{grid-area:1/1;display:flex;flex-direction:column;gap:9px;text-decoration:none;color:var(--ink);
    opacity:0;visibility:hidden;transition:opacity .6s ease, visibility 0s linear .6s;}
  .news-slide.on{opacity:1;visibility:visible;transition-delay:0s;}
  .news-slide img{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;background:var(--sheet-2);border-radius:0;max-height:340px;}
  
  .news-slide-words{display:flex;flex-direction:column;gap:4px;min-height:4.4em;}
  .news-slide .news-title{font-size:15px;font-weight:600;line-height:1.35;
    display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}
  .news-slide:hover .news-title{text-decoration:underline;}
  .news-slide-bar{display:flex;align-items:center;gap:6px;}
  .news-slide-bar .icon-btn{opacity:1;width:28px;height:28px;border:1px solid var(--line);}
  .news-slide-count{font-size:11px;color:var(--ink-3);min-width:4.5em;text-align:center;}
  body.zoom-open .module.is-app .news-slide img{aspect-ratio:16/9;max-height:62vh;}
  body.zoom-open .module.is-app .news-slide .news-title{font-size:20px;}

  /* ---- a clock in proportion ----
     The dial used to take every pixel its row had spare, so beside a tall
     widget it became a wall clock. It now stops at a size that reads across a
     room, and the digits stop with it. Full screen is still the big clock. */
  .today .face.today-face{max-height:150px;min-height:96px;}
  .today-time{font-size:clamp(28px,9cqw,46px);}
  .today-date{font-size:clamp(13px,3.4cqw,16px);}
  .today-facts{margin-top:10px;}
  .board-grid.zoomed .today .face.today-face,
  body.zoom-open .module.is-app .today .face.today-face{max-height:none;}

  /* the smallest clock: one line, and only as tall as that line */
  .module.t-today:has(.today.mini){align-self:start;}
  .module.t-today:has(.today.mini) .mod-body{flex:none;container-type:normal;}
  .today.mini{padding:4px 0 2px;}
  .today.mini .today-main{flex-direction:row;align-items:center;justify-content:flex-start;
    text-align:left;gap:12px;flex-wrap:nowrap;min-width:0;}
  .today.mini .face.today-face{flex:0 0 40px;width:40px;height:40px;min-height:0;max-height:none;margin:0;}
  .today.mini .today-time{font-size:26px;letter-spacing:-0.02em;flex:0 0 auto;}
  .today.mini .today-date{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;}
  .today.mini .date-long{display:none;}
  .today.mini .date-short{display:inline;}
  .today.mini .today-year,.today.mini .today-facts{display:none;}

  /* ---- a widget is an app: its name, then the section it is on ----
     The name is the label every widget has always had; the section beside it
     is quieter, in ordinary case, and it is the thing you tap to change. */
  .mod-title .mod-kind{gap:5px;padding:0 2px;margin-left:-2px;}
  .mod-section{font:500 12px/1 'Archivo',system-ui,sans-serif;color:var(--ink-2);letter-spacing:0.01em;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;}
  .mod-kind:hover .mod-section{color:var(--accent);}
  .mod-kind:hover .mod-caret{border-color:var(--accent);}
  body.zoom-open .module.is-app .mod-section{font-size:14px;}
  .mod-head > .mod-title > .mod-label{flex:0 0 auto;}

  /* On a phone the board's row gap and a margin under every widget were both
     being paid: 54px between any two widgets, more than most of them are tall.
     The gap alone is the space between widgets. */
  @media (max-width: 640px){
    .board-grid > .module{margin-bottom:0;}
  }
