@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
  --page-bg:#0E1115; --page-surface:#181C22; --page-ink:#E6E9EE;
  --page-muted:#8A93A1; --page-line:#262B33; --page-accent:#7FB6BB; --bezel:#000000;}}
:root[data-theme="dark"]{--page-bg:#0E1115; --page-surface:#181C22; --page-ink:#E6E9EE;
  --page-muted:#8A93A1; --page-line:#262B33; --page-accent:#7FB6BB; --bezel:#000000;}
/* Any class that sets display beats the browser's [hidden] rule, which is how a
   run day ended up showing a lift selector. Stated once so it cannot recur. */
[hidden]{display:none !important;}
.screen{position:relative;height:812px;border-radius:34px;overflow:hidden;
  /* four declared colours, everything else derived */
  --bg:#000000; --surface:#1C1C1E; --primary:#F5F5F7; --accent:#2997FF;
  --t2:color-mix(in srgb, var(--primary) 75%, var(--bg));
  --sunken:color-mix(in srgb, var(--primary) 9%, var(--bg));
  --line:color-mix(in srgb, var(--primary) 7%, var(--surface));
  --missed:color-mix(in srgb, var(--primary) 16%, var(--bg));
  --press:color-mix(in srgb, var(--accent) 86%, var(--primary));
  --elev:0 1px 2px color-mix(in srgb, var(--primary) 8%, transparent),
         0 8px 24px -6px color-mix(in srgb, var(--primary) 16%, transparent);
  --elev-live:0 2px 6px color-mix(in srgb, var(--accent) 22%, transparent),
              0 12px 30px -8px color-mix(in srgb, var(--accent) 34%, transparent);
  --elev-nav:0 -1px 24px color-mix(in srgb, var(--primary) 9%, transparent);
  --ease-nav:cubic-bezier(.32,.72,0,1); --ease-std:cubic-bezier(.4,0,.2,1);
  /* Leaves fast and settles, which is what makes a short move read as crisp
     rather than clipped. */
  --ease-out:cubic-bezier(.22,.61,.36,1);
  background:var(--bg);color:var(--primary);
  font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",Roboto,sans-serif;
  font-size:17px;line-height:1.45;user-select:none;-webkit-user-select:none;}
/* the banner: a sibling of the pages, so it never slides */
.appbar{position:absolute;top:26px;left:0;right:0;height:56px;z-index:5;
  display:flex;align-items:center;justify-content:space-between;padding:0 20px;
  background:var(--bg);}
.appbar h3{font-size:28px;font-weight:700;letter-spacing:-.02em;margin:0;line-height:34px;}
.appright{display:flex;align-items:center;gap:10px;}
.ringmini{position:relative;width:30px;height:30px;}
.ringmini svg{width:30px;height:30px;transform:rotate(-90deg);}
.ringmini circle{fill:none;stroke-width:4;stroke-linecap:round;}
.ringmini .trk{stroke:var(--sunken);}
.ringmini .fil{stroke:var(--primary);transition:stroke-dashoffset .42s var(--ease-nav);}
.ringmini b{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:11px;font-weight:700;font-variant-numeric:tabular-nums;}
.pill{position:relative;height:30px;padding:0 12px;border-radius:15px;
  background:var(--surface);box-shadow:var(--elev);display:flex;align-items:center;
  justify-content:center;font-size:14px;font-weight:600;
  font-variant-numeric:tabular-nums;}
/* The pill is held open by an invisible copy of the widest label the app can
   produce, so the visible one is laid over it and the width never moves. */
.pill .ghost{visibility:hidden;pointer-events:none;}
.pill #streakVal{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;}
/* content dissolves as it passes under the banner */
/* No mask. A gradient mask on the scrolling container forced Safari to
   composite the whole stack offscreen on every frame of every slide, which is
   most of why moving between pages felt heavy. The app bar is opaque, so the
   mask was buying a softness nobody could see. */
.stack{position:absolute;inset:82px 0 90px;overflow:hidden;}
.view{position:absolute;inset:0;overflow-y:auto;padding:24px 20px 20px;background:var(--bg);}
/* ============ the seven motion primitives ============
   Every transition in the app is one of these. Nothing gets its own timing,
   so a screen you have never seen still moves the way you expect. */
/* The pages cross-fade and drift a short distance, they do not slide past each
   other. Translating a whole screen width meant a grid of white cards travelling
   across the display like panels on rails, which is what read as mechanical. The
   background never moves now; only the content does, and only by 30px. */
.view.sliding{transition:transform var(--dur,.2s) var(--ease-out),
                        opacity calc(var(--dur,.2s) * .75) linear;}
/* The page ground belongs to the stack, so a view is just its cards. Two views
   overlapping mid-fade blend against one steady background instead of one
   opaque plane wiping over another. */
.stack{background:var(--bg);}
.view{background:transparent;}
/* Promoted only while actually moving. A permanent will-change keeps a layer
   alive for every view at once, which costs memory and gains nothing at rest. */
/* Promoted at rest, not at the moment they start moving. Adding will-change in
   the same frame as the transform means the compositor builds the layer during
   the first frame of the animation, which is a guaranteed hitch on exactly the
   frame you notice most. Three views is a cheap thing to keep promoted. */
.view,#session,.sheet,.mgridcal{will-change:transform;
  transform:translateZ(0);backface-visibility:hidden;}
/* 2. push: the stack sinks and dims while a full screen sits over it */
.stack{transition:transform .22s var(--ease-out),opacity .22s var(--ease-out);}
.stack.under{transform:translateX(-22%) scale(.97);opacity:.5;}
/* 3. sheet: rises once, then changes its mind in place rather than dropping */
.sheet{transition:transform .3s var(--ease-nav),height .28s var(--ease-nav);}
.sheet .swap{animation:sheetin .22s var(--ease-std) both;}
@keyframes sheetin{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}
/* 5. horizontal replace: months come from the side they live on */
.mview{position:relative;overflow:hidden;transition:height .28s var(--ease-nav);}
.mgridcal{transition:transform .28s var(--ease-nav),opacity .24s var(--ease-std);}
.mgridcal.ghost{position:absolute;left:0;right:0;top:0;pointer-events:none;}
#mTitle{transition:opacity .12s var(--ease-std);}
#mTitle.swap{opacity:0;}
/* 6. in-place morph: a card that changes what it holds changes size, it does
      not jump to the new size and hope you were not looking */
.morph{overflow:hidden;transition:height .24s var(--ease-nav);}
/* Motion is how the app explains itself. If the reader has asked for less of
   it, the explanation still has to happen, just instantly. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{transition-duration:.01ms !important;animation-duration:.01ms !important;}
}
.view::-webkit-scrollbar{display:none;}
.card{background:var(--surface);border-radius:16px;padding:16px;margin-bottom:10px;
  position:relative;box-shadow:var(--elev);}
.t2s{font-size:22px;font-weight:600;letter-spacing:-.01em;line-height:28px;}
.t3s{font-size:17px;font-weight:600;}
.sub{font-size:15px;color:var(--t2);line-height:21px;}
.sub:empty{display:none;}
.cap{font-size:11px;font-weight:600;color:var(--t2);text-transform:uppercase;letter-spacing:.08em;}
button{font-family:inherit;border:none;background:none;color:inherit;cursor:pointer;}
.cta{width:100%;height:52px;border-radius:26px;background:var(--accent);color:#fff;
  font-size:17px;font-weight:600;margin-top:16px;}
.cta:active{background:var(--press);}
.card.live{box-shadow:var(--elev-live);}
.card.done .cta{background:var(--sunken);color:var(--t2);}
/* The only button in the app that is not the accent. Removing a log is the one
   action that subtracts, and it should never look like the thing you came to do. */
.cta.quiet{background:var(--sunken);color:var(--t2);}
.cta.quiet:active{background:var(--missed);}
.badge{height:22px;padding:0 8px;border-radius:6px;background:var(--sunken);color:var(--t2);
  font-size:13px;font-weight:600;display:inline-flex;align-items:center;}
/* The last seven days against the seven before them, at the top of the page you
   open every evening. A week is the shortest window that survives one bad day,
   which is why it is here and the thirty-day view is on Progress. */
.week2{display:flex;gap:10px;}
.card.w2{flex:1;min-width:0;padding:11px 12px 12px;}
.card.w2 .cardtop{min-height:18px;}
.card.w2 .cap{font-size:10px;letter-spacing:.06em;}
.w2v{font-size:20px;font-weight:700;letter-spacing:-.02em;margin-top:4px;
  font-variant-numeric:tabular-nums;white-space:nowrap;}
.dlt{font-size:12px;font-weight:600;color:var(--t2);font-variant-numeric:tabular-nums;}
/* Every card on Today opens with a title row. Movement carries the two nested
   boxes there; Weight carries nothing, which is what makes them read as the
   same object with different contents. */
.cardtop{display:flex;align-items:center;justify-content:space-between;min-height:32px;}
/* Which session, chosen before you start rather than after. It is a control,
   not a title: sunken, full width, with a chevron, so it reads as something you
   change rather than something the app is telling you. */
.select{width:100%;height:46px;border-radius:23px;background:var(--sunken);
  display:flex;align-items:center;justify-content:space-between;padding:0 14px;
  margin-top:12px;font-size:16px;font-weight:600;color:var(--primary);}
.select svg{width:20px;height:20px;fill:none;stroke:var(--t2);stroke-width:2.2;
  stroke-linecap:round;stroke-linejoin:round;}
.select:active{background:var(--missed);}
/* A sunken well with two boxes inside it, so the pair reads as nested rather
   than as two loose buttons floating on the card. */
.segs{display:flex;gap:3px;background:var(--sunken);padding:3px;border-radius:11px;}
.seg{height:30px;padding:0 13px;border-radius:8px;background:transparent;color:var(--t2);
  font-size:14px;font-weight:600;display:flex;align-items:center;
  transition:background .16s var(--ease-std),color .16s var(--ease-std),box-shadow .16s var(--ease-std);}
.seg.on{background:var(--surface);color:var(--primary);box-shadow:var(--elev);}
/* Done is carried by colour, never by an added element: anything appended
   changes the chip's width, and a control that resizes reads as a glitch. */
.seg.did{color:var(--primary);}
/* Selected AND already done. This was color:#fff, which is white text on the
   white chip .seg.on paints, so the label vanished the moment a session had been
   logged. The chip says selected; the accent says done. */
.seg.on.did{color:var(--accent);}
/* The session name is the picker. Today's is the default and no other day is
   ever described as wrong: swapping Saturday's session for Friday's is a
   normal thing to do, not a deviation the app has an opinion about. */
.pick{display:flex;align-items:center;gap:4px;margin-top:10px;padding:0;}
.pick svg{width:20px;height:20px;fill:none;stroke:var(--t2);stroke-width:2.2;
  stroke-linecap:round;stroke-linejoin:round;}
.pick:active{opacity:.55;}
.pickrow{display:flex;align-items:center;gap:12px;height:60px;width:100%;text-align:left;
  position:relative;}
.pickrow + .pickrow::before{content:"";position:absolute;left:0;right:0;top:0;height:1px;
  background:var(--line);}
.pickrow .dayp{font-size:12px;font-weight:700;color:var(--t2);text-transform:uppercase;
  letter-spacing:.08em;width:34px;flex:0 0 34px;}
.pickrow .nm{font-size:17px;font-weight:500;}
.pickrow .ct{margin-left:auto;font-size:14px;color:var(--t2);}
.pickrow.on .nm{color:var(--accent);font-weight:600;}
/* ============ logging, carried over from the race app ============
   One collapsible card per exercise. Closed, the checkbox confirms it as
   prescribed in a single tap. Open, it is the set-by-set grid you already
   built: reps by weight, a unit toggle, add set, and last time's weight.
   Volume recalculates on every keystroke, exactly as it did before. */
.lcard{background:var(--surface);border-radius:16px;margin-bottom:8px;overflow:hidden;
  box-shadow:var(--elev);}
.lhead{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;text-align:left;}
.lhead:active{background:var(--sunken);}
.lbox{flex:0 0 26px;width:26px;height:26px;border-radius:7px;background:var(--sunken);
  display:flex;align-items:center;justify-content:center;transition:background .14s var(--ease-std);}
.lbox svg{width:14px;height:14px;fill:none;stroke:var(--bg);stroke-width:2.6;stroke-linecap:round;
  stroke-linejoin:round;stroke-dasharray:24;stroke-dashoffset:24;}
.lcard.on .lbox{background:var(--primary);}
.lcard.on .lbox svg{stroke-dashoffset:0;transition:stroke-dashoffset .18s var(--ease-std) .04s;}
.lmid{min-width:0;flex:1;}
/* The two-frame exercise animations, vendored from free-exercise-db in the race
   app and carried over whole. They alternate only while a card is open, because
   a grid of seven looping images is a fairground, not a training log. */
.exthumb{flex:0 0 40px;width:40px;height:40px;border-radius:8px;object-fit:cover;
  background:var(--sunken);}
.exduo{display:flex;gap:8px;align-items:stretch;}
.exduo .exfilm{flex:1;min-width:0;}
.exmap{width:90px;height:158px;border-radius:12px;object-fit:cover;background:var(--sunken);flex:none;}
.exfilm{width:100%;height:158px;border-radius:12px;object-fit:cover;background:var(--sunken);
  margin-bottom:12px;display:block;}
.exname{font-size:16px;font-weight:500;}
.lcard.on .exname{color:var(--t2);}
.exsub{font-size:13px;color:var(--t2);margin-top:1px;}
.lcaret{width:18px;height:18px;fill:none;stroke:var(--t2);stroke-width:2.2;stroke-linecap:round;
  stroke-linejoin:round;transition:transform .22s var(--ease-nav);flex:0 0 18px;}
.lcard.open .lcaret{transform:rotate(180deg);}
.lbody{height:0;overflow:hidden;transition:height .24s var(--ease-nav);}
.lpad{padding:0 14px 14px;}
/* ================= the run screen =================
   The old version was four identical grey boxes with uppercase labels above
   each one, and two of them were not even inputs. Three fixes:
   pace becomes the hero, because it is the thing you actually want to see;
   the fields get real hierarchy with the unit inside them; and the ten-button
   effort wall becomes a scale that looks like a scale. */
.hero{background:var(--surface);border-radius:18px;padding:22px 20px 18px;
  box-shadow:var(--elev);text-align:center;margin-bottom:14px;}
.hero b{display:block;font-size:48px;font-weight:700;line-height:1;letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;color:var(--t2);transition:color .2s var(--ease-std);}
.hero.live b{color:var(--accent);}
.hero .hl{font-size:11px;font-weight:700;letter-spacing:.11em;text-transform:uppercase;
  color:var(--t2);margin-top:7px;}
.frow{display:flex;gap:10px;margin-bottom:14px;}
.field{flex:1;min-width:0;background:var(--surface);border-radius:14px;padding:11px 14px 12px;
  box-shadow:var(--elev);display:block;transition:box-shadow .18s var(--ease-std);}
.field:focus-within{box-shadow:var(--elev-live);}
.field .flab{display:block;font-size:12px;font-weight:600;color:var(--t2);margin-bottom:2px;}
.field .fin{display:flex;align-items:baseline;gap:5px;}
.field input{flex:1;min-width:0;width:100%;background:none;border:none;padding:0;
  font-family:inherit;font-size:25px;font-weight:700;letter-spacing:-.01em;
  color:var(--primary);font-variant-numeric:tabular-nums;}
.field input:focus{outline:none;}
.field input::placeholder{color:var(--t2);font-weight:600;opacity:.5;}
.field em{font-style:normal;font-size:14px;font-weight:600;color:var(--t2);}
/* ============ the set grid ============
   Brought into the same language as the run screen: labels stated once at the
   top rather than on every row, values large and centred, units carried by the
   column head instead of a multiplication sign nobody needed. */
.unittog{display:flex;gap:3px;background:var(--sunken);padding:3px;border-radius:10px;
  width:-moz-fit-content;width:fit-content;margin-bottom:14px;}
.unittog button{height:28px;padding:0 14px;border-radius:7px;background:transparent;
  color:var(--t2);font-size:13px;font-weight:600;
  transition:background .16s var(--ease-std),color .16s var(--ease-std);}
.unittog button.on{background:var(--surface);color:var(--primary);box-shadow:var(--elev);}
.sethead{display:flex;align-items:center;gap:8px;padding:0 0 7px;}
.sethead .sn{width:22px;flex:0 0 22px;}
.sethead span{flex:1;text-align:center;font-size:11px;font-weight:700;
  letter-spacing:.09em;text-transform:uppercase;color:var(--t2);}
.setrow{display:flex;align-items:center;gap:8px;margin-bottom:7px;}
.setrow .sn{width:22px;flex:0 0 22px;text-align:center;font-size:13px;font-weight:700;
  color:var(--t2);font-variant-numeric:tabular-nums;}
.setrow input{flex:1;min-width:0;width:100%;height:48px;border-radius:12px;
  background:var(--sunken);border:none;text-align:center;font-family:inherit;
  font-size:19px;font-weight:700;color:var(--primary);font-variant-numeric:tabular-nums;
  transition:box-shadow .16s var(--ease-std);}
.setrow input:focus{outline:none;box-shadow:0 0 0 2px var(--accent) inset;}
.setrow input::placeholder{color:var(--t2);font-weight:600;opacity:.5;}
.addset{width:100%;height:42px;border-radius:21px;background:var(--sunken);
  color:var(--accent);font-size:14px;font-weight:600;margin-top:3px;padding:0;}
.addset:active{background:var(--missed);}
/* ============ the two clocks a lifting session needs ============
   One counts up, because a session has a length and you want to know it.
   One counts down, because sixty seconds between sets is the difference
   between training and pottering. Neither makes a sound: this app has no
   audio, and a gym is loud anyway. */
/* Above the footer, not over it. Sitting on top of it hid "Log this session"
   for the whole rest period, which was tolerable at a flat 60 seconds and became
   three minutes once rest was prescribed per exercise. The one button you need
   at the end of a session must never be behind a countdown. */
/* Above the footer, and genuinely absent when it is not counting.
   A slide-away needs somewhere to slide to. Anchored at bottom:100% there is
   nowhere below to go, so translating it down by its own height put it straight
   over the log button and left it there permanently. Hidden is now visibility
   plus opacity, which cannot be half-right. */
/* Shown or not shown, with nothing in between.

   This panel has been wrong three times in one day: it covered the log button,
   then a slide-away with nowhere to slide to left it permanently over the footer,
   then an opacity fade left it in whichever state the transition had reached. All
   three were animation states pretending to be visibility states. display is not
   animatable, which is exactly the property this needs: there is no half-open
   rest timer, and the one button behind it is the one that saves a session. */
#rest{position:absolute;left:0;right:0;bottom:100%;background:var(--surface);
  box-shadow:var(--elev-nav);padding:16px 20px 18px;z-index:2;display:none;}
#rest.show{display:block;}
.restrow{display:flex;align-items:center;gap:12px;}
.restrow .rt{flex:1;text-align:center;margin-left:44px;font-size:46px;font-weight:700;
  font-variant-numeric:tabular-nums;letter-spacing:-.03em;color:var(--accent);line-height:1;}
.restrow .rs{width:44px;flex:0 0 44px;text-align:right;font-size:15px;font-weight:600;
  color:var(--t2);padding:6px 0;}
.restbar{height:6px;border-radius:3px;background:var(--sunken);margin-top:14px;overflow:hidden;}
.restbar i{display:block;height:100%;background:var(--accent);border-radius:3px;
  transition:width .25s linear;}
.efflab{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:8px;}
.efflab span{font-size:12px;font-weight:600;color:var(--t2);}
.efflab b{font-size:15px;font-weight:700;color:var(--accent);font-variant-numeric:tabular-nums;}
.scale{display:flex;align-items:flex-end;gap:4px;height:54px;}
.scale button{flex:1;background:var(--sunken);border-radius:5px 5px 3px 3px;padding:0;
  transition:background .14s var(--ease-std);}
.scale button.on{background:var(--accent);}
.ends{display:flex;justify-content:space-between;margin-top:7px;}
.ends span{font-size:12px;font-weight:500;color:var(--t2);}
/* Two kinds of work measured two different ways, so they are two lists, not one.
   Lifting is tonnage, running is distance, and the totals live in the headers so
   the detail stays folded away until asked for. */
.ghead{display:flex;align-items:center;gap:10px;width:100%;height:56px;padding:0 16px;
  text-align:left;}
.ghead .tot{font-size:16px;font-weight:600;
  font-variant-numeric:tabular-nums;color:var(--accent);}
/* The change is never coloured for direction. Up in tonnage is good, down in
   weight is good, and an app that colours one green would have to colour the
   other red, which this one does not own a red for. */
.ghead .dlt{margin-left:auto;}
.ghead .chev{width:20px;height:20px;fill:none;stroke:var(--t2);stroke-width:2.2;
  stroke-linecap:round;stroke-linejoin:round;transition:transform .22s var(--ease-nav);}
.ghead.open .chev{transform:rotate(180deg);}
.ghead:active{background:var(--sunken);}
.gbody{height:0;overflow:hidden;transition:height .26s var(--ease-nav);}
/* Every session ever logged, newest first, each one openable. This is the only
   place in the app that shows a day other than today. */
.lg{display:flex;align-items:center;gap:12px;height:62px;width:100%;text-align:left;
  padding:0 16px;position:relative;transition:background .12s var(--ease-std);}
.lg:active{background:var(--sunken);}
.lg + .lg::before{content:"";position:absolute;left:16px;right:0;top:0;height:1px;
  background:var(--line);}
/* Weekday on top, date under it. "Fri 21" then "Aug" split the date across two
   lines in the wrong place; the day is what you scan for and the date is what
   you confirm with. */
.lg .dt{width:62px;flex:0 0 62px;}
.lg .dt b{display:block;font-size:11px;font-weight:700;color:var(--t2);
  text-transform:uppercase;letter-spacing:.07em;line-height:1.2;}
.lg .dt span{display:block;font-size:13px;font-weight:500;color:var(--t2);
  line-height:1.25;margin-top:1px;white-space:nowrap;}
.lg .nm{font-size:16px;font-weight:500;}
.lg .nm:first-child{font-size:17px;font-weight:600;letter-spacing:-.01em;}
.lg .rt{margin-left:auto;font-size:14px;color:var(--t2);font-variant-numeric:tabular-nums;}
.lg.empty{color:var(--t2);font-size:15px;height:52px;}
/* Five squares, not five rows. Same language as the weeks grid on Progress. */
/* Three across, two down. Six items in a single row would be 52px wide each,
   which is too narrow for a word like Yoghurt to sit on one line. */
.mgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:12px;}
.mcell{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px;
  padding:0;transition:opacity .1s var(--ease-std);}
.mcell.press{opacity:.55;}
.mcell i{width:100%;height:46px;border-radius:10px;background:var(--sunken);display:flex;
  align-items:center;justify-content:center;transition:background .14s var(--ease-std);}
.mcell.on i{background:var(--primary);}
.mcell i svg{width:18px;height:18px;fill:none;stroke:var(--bg);stroke-width:2.6;
  stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:24;stroke-dashoffset:24;}
.mcell.on i svg{stroke-dashoffset:0;transition:stroke-dashoffset .18s var(--ease-std) .04s;}
.mcell.pop i{animation:pop .16s var(--ease-std);}
.mcell span{font-size:12px;font-weight:500;color:var(--t2);text-align:center;line-height:1.2;}
.mcell.on span{color:var(--primary);}
.mcell.pulse i{box-shadow:0 0 0 2px var(--accent);animation:pulseb 1.8s ease-in-out infinite;}
/* meals */
.flat{padding:0;overflow:hidden;}
.mh{height:46px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;}
.row{display:flex;align-items:center;gap:14px;height:70px;padding:0 16px;width:100%;
  text-align:left;position:relative;transition:background .12s var(--ease-std);}
.row.press{background:var(--sunken);transition-duration:.06s;}
.row + .row::before{content:"";position:absolute;left:58px;right:0;top:0;height:1px;background:var(--line);}
.box{flex:0 0 28px;width:28px;height:28px;border-radius:8px;background:var(--sunken);
  display:flex;align-items:center;justify-content:center;transition:background .14s var(--ease-std);}
.box svg{width:16px;height:16px;fill:none;stroke:var(--bg);stroke-width:2.5;stroke-linecap:round;
  stroke-linejoin:round;stroke-dasharray:24;stroke-dashoffset:24;}
.row.on .box,.lift.on .box{background:var(--primary);}
.row.on .box svg,.lift.on .box svg{stroke-dashoffset:0;transition:stroke-dashoffset .18s var(--ease-std) .04s;}
.row.pop .box,.lift.pop .box{animation:pop .16s var(--ease-std);}
@keyframes pop{0%{transform:scale(1)}50%{transform:scale(.92)}100%{transform:scale(1)}}
.mname{font-size:17px;font-weight:500;transition:color .16s var(--ease-std);}
.row.on .mname,.lift.on .mname{color:var(--t2);}
.msub{font-size:15px;color:var(--t2);margin-top:2px;white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;max-width:280px;}
.row.pulse .box{box-shadow:0 0 0 2px var(--accent);animation:pulseb 1.8s ease-in-out infinite;}
@keyframes pulseb{0%,100%{opacity:1}50%{opacity:.4}}
/* ===================== session mode ===================== */
/* Stops above the home indicator rather than running beneath it. The indicator
   paints on top, so a footer at inset:0 loses its bottom 34px. */
#session{position:absolute;inset:0 0 34px 0;z-index:14;background:var(--bg);
  transform:translateX(100%);transition:transform .22s var(--ease-out);
  display:flex;flex-direction:column;}
#session.show{transform:translateX(0);}
.shead{height:90px;padding:36px 20px 0;display:flex;align-items:flex-start;gap:12px;flex:0 0 90px;}
/* The session clock, read at arm's length between sets. */
.sclock{margin-left:auto;font-size:26px;font-weight:700;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;color:var(--primary);line-height:1.1;}
.sback{width:32px;height:32px;border-radius:16px;background:var(--surface);box-shadow:var(--elev);
  display:flex;align-items:center;justify-content:center;flex:0 0 32px;margin-top:2px;}
.sback svg{width:17px;height:17px;fill:none;stroke:var(--primary);stroke-width:2.4;
  stroke-linecap:round;stroke-linejoin:round;}
/* The day picks the session. This is how you pick a different one, and it has
   to look like a control: a dotted underline is a hint, not an affordance. */
.stitle{display:flex;align-items:center;gap:5px;padding:0;text-align:left;max-width:100%;}
.stitle svg{width:20px;height:20px;fill:none;stroke:var(--t2);stroke-width:2.2;
  stroke-linecap:round;stroke-linejoin:round;flex:0 0 20px;}
.stitle:active{opacity:.55;}
.sbody{flex:1;overflow-y:auto;padding:0 20px 20px;}
.sbody::-webkit-scrollbar{display:none;}
.sfoot{flex:0 0 auto;padding:14px 20px 16px;background:var(--bg);box-shadow:var(--elev-nav);}
.sfoot .cta{margin-top:0;height:52px;}
.sfoot .cta + .cta{margin-top:8px;}
/* Read-only is the default for anything already logged. Editing is a decision,
   so it takes a tap, and it can always be walked back. */
#sBody.ro input{pointer-events:none;}
#sBody.ro .addset,#sBody.ro .unittog{display:none;}
#sBody.ro .lbox{pointer-events:none;opacity:.85;}
#sBody.ro .scale button{pointer-events:none;}
.lift{display:flex;align-items:center;gap:14px;height:60px;width:100%;text-align:left;
  position:relative;transition:background .12s var(--ease-std);}
.lift.press{background:var(--sunken);transition-duration:.06s;}
.lift + .lift::before{content:"";position:absolute;left:56px;right:0;top:0;height:1px;background:var(--line);}
.lift .box{width:26px;height:26px;flex:0 0 26px;border-radius:7px;}
.wt{margin-left:auto;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;
  background:var(--sunken);padding:5px 10px;border-radius:8px;color:var(--t2);}
.wt.set{color:var(--primary);}
.stats{display:flex;gap:6px;}
.stat{flex:1;height:70px;background:var(--sunken);border-radius:12px;display:flex;
  flex-direction:column;align-items:center;justify-content:center;gap:2px;}
.stat b{font-size:21px;font-weight:700;}
.stat span{font-size:12px;font-weight:500;color:var(--t2);}
/* progress */
/* One month at a time, Sunday first, and you can walk back through the year.
   A rolling four-week strip could never answer "what did I do in July". */
.mnav{width:32px;height:32px;border-radius:16px;background:var(--sunken);
  display:flex;align-items:center;justify-content:center;flex:0 0 32px;}
.mnav svg{width:17px;height:17px;fill:none;stroke:var(--primary);stroke-width:2.4;
  stroke-linecap:round;stroke-linejoin:round;}
.mnav:disabled{opacity:.3;}
.mnav:active:not(:disabled){background:var(--missed);}
.cardtop #mTitle{flex:1;text-align:center;}
.mgridcal{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;}
.cday{aspect-ratio:1;border-radius:9px;background:var(--sunken);display:flex;
  align-items:center;justify-content:center;font-size:13px;font-weight:600;
  color:var(--t2);font-variant-numeric:tabular-nums;}
.cday.blank{background:transparent;}
.cday.done{background:var(--primary);color:var(--surface);}
.cday.miss{background:var(--missed);color:color-mix(in srgb,var(--primary) 45%,var(--missed));}
.cday.future{background:transparent;color:color-mix(in srgb,var(--t2) 45%,var(--bg));}
.cday.today{box-shadow:0 0 0 2px var(--accent) inset;color:var(--accent);}
.cday.today.done{color:var(--surface);}
.dowr{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;margin:14px 0 7px;}
.dw{text-align:center;font-size:11px;font-weight:700;color:var(--t2);
  text-transform:uppercase;letter-spacing:.05em;}
/* settings */
.srow{display:flex;align-items:center;justify-content:space-between;height:60px;
  padding:0 16px;position:relative;}
.srow + .srow::before{content:"";position:absolute;left:16px;right:0;top:0;height:1px;background:var(--line);}
.step{display:flex;align-items:center;width:108px;height:36px;border-radius:10px;background:var(--sunken);}
.step button{flex:0 0 32px;height:36px;color:var(--primary);font-size:17px;font-weight:600;}
.step button:disabled{opacity:.3;}
.step b{flex:1;text-align:center;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;}
.tg{width:51px;height:31px;border-radius:16px;background:var(--accent);position:relative;flex:0 0 51px;}
.tg i{position:absolute;top:2px;left:22px;width:27px;height:27px;border-radius:50%;background:#fff;
  box-shadow:0 1px 3px rgba(0,0,0,.2);transition:left .2s var(--ease-std);}
.tg.off{background:var(--sunken);}
.tg.off i{left:2px;}
.popts{overflow:hidden;height:0;transition:height .24s var(--ease-std);}
.popt{width:100%;height:48px;border-radius:24px;background:var(--sunken);color:var(--primary);
  font-size:17px;font-weight:500;margin-top:8px;}
.foot{font-size:13px;font-weight:500;color:var(--t2);text-align:center;margin-top:20px;}
/* tabs */
.tabs{position:absolute;left:0;right:0;bottom:34px;height:56px;background:var(--surface);
  display:flex;z-index:5;box-shadow:var(--elev-nav);}
.tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
  color:var(--t2);transition:color .18s var(--ease-std),opacity .06s;}
.tab svg{width:26px;height:26px;fill:none;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round;}
.tab span{font-size:11px;font-weight:500;}
.tab.on{color:var(--primary);}
.tab.on span{font-weight:600;}
.tab:active{opacity:.45;}
/* sheets, toast, lock */
#scrim{position:absolute;inset:0;background:color-mix(in srgb, var(--primary) 22%, transparent);
  opacity:0;pointer-events:none;transition:opacity .2s var(--ease-std);z-index:16;}
#scrim.show{opacity:1;pointer-events:auto;}
.sheet{position:absolute;left:0;right:0;bottom:0;background:var(--surface);
  border-radius:20px 20px 0 0;z-index:17;transform:translateY(100%);
  transition:transform .28s var(--ease-nav);padding:0 20px 34px;
  box-shadow:0 -6px 32px color-mix(in srgb, var(--primary) 18%, transparent);}
.sheet.show{transform:translateY(0);}
.grab{width:36px;height:5px;border-radius:3px;background:var(--sunken);margin:8px auto 0;}
.sheet h4{font-size:20px;font-weight:600;margin:20px 0 0;letter-spacing:-.01em;}
.big{width:100%;height:54px;border-radius:27px;background:var(--accent);color:#fff;
  font-size:17px;font-weight:600;margin-top:12px;}
/* The accent always sits on the safe path. Grey is reserved for the action that
   takes something away, which is the same rule the session footer follows. */
.big.quiet{background:var(--sunken);color:var(--t2);}
.cbopt{width:100%;height:56px;border-radius:28px;margin-top:10px;background:var(--accent);
  color:#fff;font-size:17px;font-weight:600;}
#toast{position:absolute;left:20px;right:20px;bottom:104px;background:var(--primary);color:var(--bg);
  border-radius:12px;padding:12px 16px;font-size:15px;font-weight:500;z-index:20;
  opacity:0;transform:translateY(8px);transition:opacity .2s,transform .2s;pointer-events:none;}
#toast.show{opacity:1;transform:translateY(0);}
/* An update banner is the one toast you are meant to tap, so it waits for you
   and does not fade on its own. */
#toast.tap{pointer-events:auto;background:var(--accent);font-weight:600;}
/* The one banner that is not an offer. A stale build is an inconvenience; a
   phone that cannot back anything up is data with a single copy, so it does not
   borrow the accent that means "tap me". */
#toast.warn{background:var(--primary);pointer-events:none;}
#lock{position:absolute;inset:0;z-index:22;background:#26343A;opacity:0;pointer-events:none;
  transition:opacity .3s var(--ease-std);display:flex;flex-direction:column;align-items:center;padding-top:70px;}
#lock.show{opacity:1;pointer-events:auto;}
#lock .clock{color:#fff;font-size:66px;font-weight:600;line-height:1;letter-spacing:-.03em;}
#lock .lday{color:rgba(255,255,255,.75);font-size:16px;margin-top:6px;}
.notif{width:340px;margin-top:52px;background:rgba(255,255,255,.92);border-radius:14px;padding:12px 14px;
  display:flex;gap:10px;cursor:pointer;transform:translateY(-14px) scale(.97);opacity:0;
  transition:all .34s var(--ease-nav);}
.notif.in{transform:none;opacity:1;}
.nicon{width:38px;height:38px;border-radius:9px;background:var(--accent);color:#fff;flex:0 0 38px;
  display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:700;}
.napp{font-size:12px;color:rgba(0,0,0,.5);text-transform:uppercase;letter-spacing:.04em;}
.ntitle{font-size:15px;font-weight:600;color:#000;}
.nbody{font-size:15px;color:#1c1c1e;line-height:1.3;}
.nhint{color:rgba(255,255,255,.55);font-size:13px;margin-top:18px;}
.group{margin-bottom:22px;}
.group h3{font-size:11px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--page-muted);margin:0 0 8px;}
.btns{display:flex;flex-wrap:wrap;gap:8px;}
.ctl{background:var(--page-surface);border:1px solid var(--page-line);color:var(--page-ink);
  border-radius:8px;padding:8px 12px;font-size:14px;font-weight:500;}
.ctl:hover{border-color:var(--page-accent);}
.ctl:focus-visible{outline:2px solid var(--page-accent);outline-offset:2px;}
.ctl.on{background:var(--page-accent);border-color:var(--page-accent);color:#fff;}
.hist{display:flex;align-items:center;gap:5px;margin-bottom:12px;}
.hist .dot{width:13px;height:13px;border-radius:4px;background:var(--page-line);display:block;}
.hist .dot.on{background:var(--page-accent);}
.hist b{margin-left:8px;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;}
.legend{font-size:14px;color:var(--page-muted);line-height:1.65;}
.legend b{color:var(--page-ink);font-weight:600;}
@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms !important;transition-duration:.01ms !important;}}

/* ============ the real device, not a phone drawn on a page ============
   The prototype lived inside a 402x812 rectangle with a fake status bar and a
   fake home indicator. On the actual phone those belong to iOS, so the app
   fills the viewport and the safe-area insets decide what it must keep clear. */
html,body{height:100%;margin:0;padding:0;background:#000000;
  overflow:hidden;overscroll-behavior:none;
  -webkit-tap-highlight-color:transparent;}
*{box-sizing:border-box;}
.screen{position:fixed;inset:0;height:auto;border-radius:0;
  --bg:#000000; --surface:#1C1C1E; --primary:#F5F5F7; --accent:#2997FF;}

.appbar{top:env(safe-area-inset-top,0px);}
.stack{inset:calc(56px + env(safe-area-inset-top,0px)) 0
             calc(56px + env(safe-area-inset-bottom,0px));}
.tabs{bottom:0;height:auto;padding-bottom:env(safe-area-inset-bottom,0px);}
.tabs .tab{height:56px;}
#session{inset:0;padding-bottom:env(safe-area-inset-bottom,0px);}
.sheet{padding-bottom:calc(20px + env(safe-area-inset-bottom,0px));}
#toast{bottom:calc(80px + env(safe-area-inset-bottom,0px));}

/* Nothing in a logging app wants a text cursor or a callout menu, except the
   fields you actually type into. */
input{user-select:text;-webkit-user-select:text;-webkit-touch-callout:none;}

/* Containment. A card's layout and paint stop at its own edges, so animating
   one group open does not make Safari re-lay-out everything below it. */
.card{contain:layout paint style;}
.lg,.cday,.mcell{contain:layout style;}

/* Removes the tap disambiguation delay Safari otherwise keeps for double-tap
   zoom. Every one of these is a single-tap target. */
button,.tab,.lg,.mcell,.cday,.seg,.ghead,.mnav{touch-action:manipulation;}


/* ============ the meals screen ============
   The same push primitive the session uses, so entering meals and entering a
   workout feel like the same gesture on the same app. */
#meals{position:absolute;inset:0;z-index:14;background:var(--bg);
  transform:translateX(100%);transition:transform .22s var(--ease-out);
  display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom,0px);
  will-change:transform;backface-visibility:hidden;}
#meals.show{transform:translateX(0);}

.mrow{background:var(--surface);border-radius:16px;box-shadow:var(--elev);
  margin-bottom:10px;overflow:hidden;}
.mhead{display:flex;align-items:center;gap:12px;width:100%;padding:14px 16px;text-align:left;}
.mrow .lmid{flex:1;min-width:0;}
.mrow .exname{font-size:17px;font-weight:600;letter-spacing:-.01em;}
.mrow .exsub{font-size:13px;color:var(--t2);font-variant-numeric:tabular-nums;margin-top:2px;}
.mqty{font-size:15px;font-weight:700;color:var(--accent);font-variant-numeric:tabular-nums;
  min-width:28px;text-align:right;}
.mbody{height:0;overflow:hidden;transition:height .24s var(--ease-nav);}
.mpad{padding:0 16px 14px;}

/* The tick was in the markup and invisible: the checkmark's stroke is revealed
   by `.lcard.on`, and these rows are not lcards. */
.mrow.on .lbox{background:var(--primary);}
.mrow.on .lbox svg{stroke-dashoffset:0;transition:stroke-dashoffset .18s var(--ease-std) .04s;}

/* The day, at the top, before any of the individual meals. */
.mtot{background:var(--surface);border-radius:16px;box-shadow:var(--elev);
  padding:16px 18px 14px;margin-bottom:14px;}
.mtot b{font-size:38px;font-weight:700;letter-spacing:-.03em;line-height:1;
  font-variant-numeric:tabular-nums;}
.mtot .mtl{font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--t2);margin-left:8px;}
.mbar{display:flex;height:8px;border-radius:4px;overflow:hidden;margin:14px 0 10px;
  background:var(--sunken);}
.mbar i{display:block;transition:flex-grow .28s var(--ease-out);}
.mbar .p{background:var(--primary);}
.mbar .c{background:var(--accent);}
.mbar .f{background:color-mix(in srgb, var(--primary) 34%, var(--bg));}
/* Three words fit on one line at 402px; a wrap would be worse than a shorter
   gap, so the gap gives first. */
.mleg{display:flex;gap:18px;flex-wrap:nowrap;}
.mleg span{display:flex;align-items:center;gap:5px;font-size:13px;font-weight:600;white-space:nowrap;
  color:var(--t2);font-variant-numeric:tabular-nums;}
.mleg i{width:9px;height:9px;border-radius:3px;display:block;}
.mleg b{font-size:15px;font-weight:700;color:var(--primary);letter-spacing:-.01em;}

/* The portion opens under the meal, and the tick only lands once you confirm it. */
.qtyrow{display:flex;align-items:center;gap:8px;background:var(--sunken);
  border-radius:10px;padding:5px;margin-bottom:10px;}
.qbtn{width:38px;height:36px;border-radius:8px;background:var(--surface);box-shadow:var(--elev);
  font-size:20px;font-weight:600;color:var(--primary);display:flex;
  align-items:center;justify-content:center;}
.qbtn:active{background:var(--missed);}
.qbtn:disabled{opacity:.3;}
.qval{flex:1;text-align:center;font-size:20px;font-weight:700;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;}
.mpad .cta{height:46px;font-size:16px;}
.qrm{width:100%;height:38px;border-radius:10px;background:var(--sunken);
  color:var(--t2);font-size:14px;font-weight:600;margin-top:7px;}

/* Content scrolling up under the app bar fades out instead of being cut off at
   a hard edge. This is a single static gradient painted once, not a mask on the
   scrolling container: the mask version cost a full offscreen composite of the
   whole stack on every frame, which is what made moving between pages heavy. */
.topfade{position:absolute;left:0;right:0;z-index:4;pointer-events:none;
  top:calc(56px + env(safe-area-inset-top,0px));height:26px;
  background:linear-gradient(to bottom, var(--bg) 0%,
              color-mix(in srgb, var(--bg) 55%, transparent) 55%, transparent 100%);}

/* The tick is a control, so it has to look pressable and answer to a tap. */
.mrow .lbox{cursor:pointer;transition:background .14s var(--ease-std);}
.mrow.on .lbox:active{background:var(--press);}

/* Anything eaten outside the plan. Deliberately plainer than a planned meal, so
   the shape of the day still reads as "the plan, plus these". */
.addextra{width:100%;height:48px;border-radius:24px;background:var(--sunken);
  color:var(--accent);font-size:15px;font-weight:600;margin-top:4px;}
.addextra:active{background:var(--missed);}
/* Delete sits behind the row and is only reachable by swiping it aside. An
   always-visible destructive control on every row is one mis-tap from losing a
   record; a deliberate gesture is not. */
/* An extra looks exactly like a planned meal, because it is one: something you
   ate today. The only difference is that it can be deleted, and that lives
   behind the row rather than on it. */
.xwrap{position:relative;margin-bottom:10px;}
/* The row travels 10px further than the button is wide, so the two are separated
   by the same gap that separates every other pair of cards. Flush against each
   other they read as one control that has been cut in half. */
.xdel{position:absolute;top:0;bottom:0;right:0;width:88px;border-radius:16px;
  background:color-mix(in srgb, var(--primary) 12%, var(--bg));
  color:var(--primary);font-size:15px;font-weight:600;
  display:flex;align-items:center;justify-content:center;
  opacity:0;transition:opacity .14s var(--ease-std);}
.xwrap.open .xdel{opacity:1;}
.xdel:active{background:color-mix(in srgb, var(--primary) 22%, var(--bg));}
.xwrap .xrow{margin-bottom:0;position:relative;
  transition:transform .22s var(--ease-out);touch-action:pan-y;}
.xwrap.open .xrow{transform:translateX(-98px);}
.xnote{font-size:14px;color:var(--t2);line-height:1.4;margin:12px 0 4px;}


/* The notification state reads as a sentence, not a status chip. It is the
   only text on the page and it has to be able to say something useful. */
.nstate{font-size:15px;line-height:1.45;color:var(--t2);margin:10px 0 14px;}
/* On: the same button in the same colour, already pressed. A status does not
   get a colour of its own here; it gets a weight of its own. */
.cta.done{background:color-mix(in srgb, var(--accent) 13%, var(--bg));
  color:var(--accent);box-shadow:none;}
.cta.done:active{background:color-mix(in srgb, var(--accent) 20%, var(--bg));}

/* ===================== the voice =====================
   He arrives full screen, in greyscale, and then sits down as an ordinary card.
   Everything about the resting state is borrowed from .card rather than
   redeclared, so the day the card styling changes this changes with it and
   cannot drift into looking like a foreign object parked on the page. */
/* padding, radius and margin all come from .card. Only the internal
   arrangement is stated here. */
.vcard{display:flex; align-items:center; gap:14px; min-height:104px;}
.vshot{width:72px; height:72px; border-radius:14px; overflow:hidden; flex:0 0 72px;
  background:var(--missed);}
.vshot img{width:100%; height:100%; object-fit:cover; display:block;
  filter:grayscale(1);}
.vsay{font-size:15px; line-height:1.34; font-weight:600; letter-spacing:-.005em;
  color:var(--primary);}

.vhero{
  position:absolute; inset:0; contain:paint; z-index:60; overflow:hidden; background:var(--surface);
  border-radius:0;
  transition:transform var(--vd,.62s) var(--ease-nav),
             width var(--vd,.62s) var(--ease-nav),
             height var(--vd,.62s) var(--ease-nav),
             border-radius var(--vd,.62s) var(--ease-nav),
             box-shadow .34s ease .26s;
  transform-origin:top left; will-change:transform,width,height;
}
.vhshot{position:absolute; inset:0; overflow:hidden; background:#0D1214;
  transition:top var(--vd,.62s) var(--ease-nav), left var(--vd,.62s) var(--ease-nav),
             width var(--vd,.62s) var(--ease-nav), height var(--vd,.62s) var(--ease-nav),
             border-radius var(--vd,.62s) var(--ease-nav);}
.vhshot img{position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; display:block; filter:grayscale(1);}
.vhscrim{position:absolute; inset:0; transition:opacity .28s ease;
  background:linear-gradient(to bottom, rgba(13,18,20,.34) 0%, rgba(13,18,20,.06) 24%,
             rgba(13,18,20,.14) 42%, rgba(13,18,20,.62) 60%,
             rgba(13,18,20,.93) 80%, #0D1214 100%);}
/* The photograph morphs, because it has to be the same photograph. The sentence
   cross-fades: type cannot travel 31px to 15px, or white to dark, without going
   soft or passing through an unreadable grey on the way. */
.vhsay{position:absolute; left:34px; right:34px;
  bottom:calc(78px + env(safe-area-inset-bottom,0px));
  color:#F3F6F6; transition:opacity .2s ease;}
/* Four sentences is a paragraph, not a headline, and it has to be read rather
   than glanced at. So the type comes down and the sentences are separated: at
   headline size the whole message climbs over his eyes, and the one thing the
   veil must not cover is his face. */
.vhsay p{margin:0 0 15px; font-size:19px; line-height:1.44; font-weight:600;
  letter-spacing:-.004em; text-wrap:pretty;
  text-shadow:0 1px 14px rgba(13,18,20,.75);}
.vhsay p:last-child{margin-bottom:0;}
.vhwho{position:absolute; left:34px;
  bottom:calc(44px + env(safe-area-inset-bottom,0px));
  color:#93B3B6; font-size:13px; letter-spacing:.15em; text-transform:uppercase;
  transition:opacity .18s ease;}
.vhero.go .vhscrim, .vhero.go .vhsay, .vhero.go .vhwho{opacity:0;}
.vhero.go{box-shadow:var(--elev);}
/* The three who say nothing get no veil, no words and no attribution: there is
   nothing to make legible, so anything laid over the picture is in the way. */
.vhero.bare .vhscrim, .vhero.bare .vhsay, .vhero.bare .vhwho{display:none;}
/* The dissolve. Opacity on a promoted layer, so it is a compositor fade rather
   than a repaint of a full screen photograph every frame. */
.vhero{opacity:1;}
.vhero.out{opacity:0;}
.vhero.out, .vhero.bare{transition:opacity var(--vfade,1s) linear;}
.vhero.bare{will-change:opacity;}
.screen.intro .stack, .screen.intro .appbar, .screen.intro .tabs{opacity:0;}
/* Nothing at all until the picture is decoded, not even the ground the
   cards sit on, so the first thing that appears is the photograph. */
.screen.intro{background:var(--bg);}
.screen .stack, .screen .appbar, .screen .tabs{transition:opacity .34s ease .2s;}
@media (prefers-reduced-motion: reduce){
  .vhero, .vhshot, .vhsay, .vhwho, .vhscrim{transition-duration:1ms !important;}
}

/* A scheduled rest day. Visible, so the month reads as a plan rather than a
   record of failure, but quieter than a day that was actually trained. */
.cal .rest{background:transparent;color:color-mix(in srgb, var(--primary) 30%, var(--bg));
  box-shadow:inset 0 0 0 1px var(--line);}
/* A rest day that is also today keeps the today treatment. Without this, the
   rest rule wins the specificity tie by file order and today's cell fades to a
   ghost, which reads as the calendar skipping the date. */
.cal .rest.today{color:var(--accent);box-shadow:0 0 0 2px var(--accent) inset;}

/* An exercise with no film collapses rather than leaving a grey box. The element
   is always rendered because the films arrive after the first paint and need
   somewhere to land; it just has no src until they do. */
img.exthumb:not([src]),img.exfilm:not([src]),img.exmap:not([src]){display:none;}

/* A meal with no macros, vitamins being the only one, has no sub-line. Without
   this the empty div still holds its line-height open and the name sits high. */
.exsub:empty{display:none;}

/* ============ shine ============
   A material quality, not a coat of paint: every raised surface carries a soft
   specular highlight, faint at rest and brighter the instant a finger lands on
   it. --mx/--my are written by the pointer tracker below; the default position
   (upper-left) is what the highlight falls back to on a surface nobody is
   touching, as if lit from one fixed light rather than lit by nothing. */
.card,.lcard,.mrow,.mtot,.hero,.field{position:relative;isolation:isolate;}
.mtot,.hero,.field{overflow:hidden;}
.card::before,.lcard::before,.mrow::before,.mtot::before,.hero::before,.field::before{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:radial-gradient(160px circle at var(--mx,26%) var(--my,-12%),
              rgba(255,255,255,.16), transparent 72%);
  opacity:.55;transition:opacity .25s var(--ease-std);
}
.card.touching::before,.lcard.touching::before,.mrow.touching::before,
.mtot.touching::before,.hero.touching::before,.field.touching::before{opacity:1;}

/* The three buttons that ask for a tap rather than just accepting one: a slow
   light sweep, so they read as glass catching a passing light rather than a
   flat fill. Confined to these few, not every card, because an ambient loop
   is a held note and a room full of them is noise, not music. */
.cta,.big,.cbopt{position:relative;overflow:hidden;}
.cta::after,.big::after,.cbopt::after{
  content:"";position:absolute;top:0;bottom:0;left:-60%;width:40%;
  background:linear-gradient(100deg, transparent, rgba(255,255,255,.32), transparent);
  transform:skewX(-15deg);pointer-events:none;will-change:transform;
  animation:sheen 3.2s linear infinite;
}
@keyframes sheen{
  0%{left:-60%;} 100%{left:130%;}
}
@media (prefers-reduced-motion:reduce){
  .cta::after,.big::after,.cbopt::after{animation:none;display:none;}
}

/* ============ portrait only ============
   iOS honours neither the orientation manifest member nor
   screen.orientation.lock(), so this is the only lock available to a web app:
   the content simply is not laid out sideways.

   Bounded by max-height so it targets a phone on its side and never a desktop
   window or a tablet, which are landscape by nature and not the problem. */
#rotate{display:none;}
@media (orientation:landscape) and (max-height:500px){
  #rotate{
    display:flex; position:fixed; inset:0; z-index:100;
    flex-direction:column; align-items:center; justify-content:center; gap:14px;
    background:var(--bg); color:var(--primary);
    font-size:17px; font-weight:600; letter-spacing:-.01em;
  }
  /* Hidden rather than merely covered, so nothing behind it is laid out at a
     width it was never designed for and no scroll position is lost. */
  .screen{visibility:hidden;}
}
