<kaf version="1" title="The Calndr" lang="en">

  <!-- The media host every rendition is fetched from, and the origin the presigned POST goes
       to — the bucket's own on AWS (build.md R10-D3). A built app learns its stage's pair from
       `kaf.json`'s env map and a deploy's `kaf build --env`; the defaults are the fake store's
       listener, which `npm run dev`'s host serves. -->
  <env name="MEDIA" default="http://localhost:9001"/>
  <env name="UPLOAD" default="http://localhost:9001"/>
  <allow origin="{env.MEDIA}"/>
  <allow origin="{env.UPLOAD}"/>

  <import src="kit:tokens"/>
  <import src="kit:button"/>
  <import src="kit:program-face"/>
  <import src="kit:avatar"/>
  <import src="kit:upload"/>
  <import src="./parts.kaf"/>

  <!-- The whole ask (§26.6): one line reaches every shape and service `models` publishes, and
       the channel arrives at mount — this document never states a URL. `kaf.json` proxies it. -->
  <import library="models"/>

  <import src="./locale/en.kaf" locale="en"/>
  <import src="./locale/fr.kaf" locale="fr"/>

  <!-- Every section sits in one layout screen at the index, behind the guard; a signed-out
       visitor falls back to the catalog at its own path (hub §2.1). The catalog is not the index
       because a guarded leaf at "" is tried before the path is known to be consumed, so it would
       swallow every deeper path and its fallback would loop. The catalog and a program's page are
       public (hub §2.2): reachable signed in or out, and framed by neither guard. -->
  <routes>
    <route path="catalog" screen="catalog"/>
    <route path="programs/:id" screen="program"/>
    <!-- The join page is public (hub §2.2): a visitor reads it and is sent to sign in and back.
         Two doors, one page (build.md R4-D13): the open link's token, and a manager's own road by
         the calendar's id, which the host admits for managers alone. -->
    <route path="join/c/:calendarId" screen="joinAsManager"/>
    <!-- The third door (build.md R7-D4): a personal link, which admits only the address it was
         sent to. And the manager invite's accept page (roles §4.4), public for the same reason. -->
    <route path="join/i/:invitation" screen="joinInvited"/>
    <route path="join/:token" screen="join"/>
    <route path="invite/:token" screen="acceptInvite"/>
    <!-- The one door in. Guarded on nothing but the session: what says a code is live is a
         cookie this document cannot read, and the persisted cell below is only what the screen
         is ABOUT — never what makes it safe. -->
    <route path="login" screen="login" guard="{!wasSignedIn}" fallback="/"/>
    <route path="signup" screen="signup" guard="{!wasSignedIn}" fallback="/"/>
    <!-- The code is typed into the tab the visitor never left, so the screen has to survive a
         reload: the address it belongs to is not in the URL, and a persisted cell is what makes
         the guard's answer the same before and after one. -->
    <route path="confirm" screen="confirm" guard="{!wasSignedIn && pendingEmail != ''}" fallback="/"/>
    <route path="" screen="app" guard="{wasSignedIn}" fallback="/catalog"/>
    <route path="*" screen="lost"/>
  </routes>

  <!-- ── the session ─────────────────────────────────────────────
       `me` is the ONE writer of the session truth: every other call refreshes it rather than
       deciding for itself. The persisted boolean answers the guards during the window before
       the first reply lands, so a reload of a signed-in page does not bounce through /login.
       A transport failure leaves it alone deliberately — a blinked network is not a logout. -->

  <state name="wasSignedIn" persist value="false"/>
  <rpc call="SessionService.me" auto on:done="wasSignedIn = me.data.user != null"/>

  <!-- Where a join page, or a program's tier, sends a visitor back to after signing in
       (invitations §6.1, store §2.4). Those two are the writers, so hub §2.3 stays unbuilt for
       every other page (build.md R4-D12). -->
  <state name="returnTo" persist value=""/>

  <!-- Two cells the Store and Manage sections hand each other, memory only: the organization
       Manage's Buy preselects at checkout, and the one the return page lands Manage on. -->
  <state name="buyFor" value=""/>
  <state name="manageOrg" value=""/>

  <!-- Whether this load has landed on an open date (hub §3.1). Memory, never stored: a reload is
       an opening of the root and lands again; the Play tab after that shows the blocks. -->
  <state name="landed" value="false"/>

  <!-- The address a code was mailed to. It is what the confirm screen is ABOUT, and the guard's
       only way of knowing whether there is anything to confirm. -->
  <state name="pendingEmail" persist value=""/>

  <!-- The address a sign-in code went to. Persisted for the same reason `pendingEmail` is — the
       visitor types the digits into the tab they asked from, and a reload must not lose which
       address the screen is about. It is display and nothing else: the spend carries no address. -->
  <state name="codeSentTo" persist value=""/>

  <state name="account" struct="SignupInput"/>
  <state name="confirmation" struct="ConfirmSignupInput"/>
  <state name="codeRequest" struct="LoginCodeRequest"/>
  <state name="codeEntry" struct="LoginCodeInput"/>

  <!-- The reader's language, as the catalog picks one side of a bilingual string; every string
       the catalog carries rides whole and the app shows this side (store §2.1). -->
  <derived name="lang" expr="{str.starts(locale, 'fr') ? 'fr' : 'en'}"/>
  <derived name="typeLabels" expr="{{ read: t('typeRead') }}"/>

  <!-- `args` is a bare schema-backed state path, which is what earns the host's per-field
       outcomes their way onto `.errors.PROP` (§26.7). Re-minting on done is the reset. The
       account lifecycle is its own service: a signup signs nobody in, because until the mailed
       code comes back there is no account to sign in to. -->
  <rpc call="AccountService.signup" args="{{ input: account }}" on:done="afterSignup()"/>
  <rpc call="AccountService.confirmSignup" args="{{ input: confirmation }}" on:done="afterConfirm()"/>
  <!-- The one road in: the host is never asked whether an address has an account, and answers
       the ask the same way whether or not one does. -->
  <rpc call="SessionService.requestLoginCode" args="{{ input: codeRequest }}" on:done="afterCodeSent()"/>
  <rpc call="SessionService.loginWithCode" args="{{ input: codeEntry }}" on:done="afterCodeLogin()"/>
  <rpc call="SessionService.logout" on:done="landed = false; refresh('me')"/>
  <rpc call="SessionService.logoutEverywhere" on:done="landed = false; refresh('me')"/>

  <!-- Signing in flips the guard, whose fallback is the root; a visitor a join page sent here goes
       back to it instead, once. -->
  <action name="returnIfAsked">
    if (returnTo != '') { nav.reset(returnTo); returnTo = '' }
  </action>

  <!-- An account remembers the language it was opened in: the form never asks, because the
       app the visitor is reading has already answered. -->
  <action name="createAccount">
    account.locale = str.starts(locale, 'fr') ? 'fr' : 'en';
    if (account.valid) { send('signup') }
  </action>

  <!-- Signing up flips no guard, so this is the one navigation in the app that is not a guard's
       own doing: there is nothing about the visitor that changed for a guard to notice. -->
  <action name="afterSignup">
    pendingEmail = account.email;
    account = SignupInput();
    nav.reset('/confirm')
  </action>

  <!-- The address is stamped from the persisted cell rather than bound to a field: the form
       carries the code and only the code, so the screen answers the same after a reload as
       before one — a second copy would not survive it and would lock the button. -->
  <action name="submitCode">
    confirmation.email = pendingEmail;
    if (confirmation.valid) { send('confirmSignup') }
  </action>

  <action name="afterConfirm">
    pendingEmail = '';
    confirmation = ConfirmSignupInput();
    refresh('me');
    returnIfAsked()
  </action>

  <action name="askForCode">
    if (codeRequest.valid) { send('requestLoginCode') }
  </action>

  <!-- The answer is the same for an address with an account and one without, so this runs on
       every success and the screen says what was DONE rather than what was found. -->
  <action name="afterCodeSent">
    codeSentTo = codeRequest.email;
    codeRequest = LoginCodeRequest()
  </action>

  <action name="submitLoginCode">
    if (codeEntry.valid) { send('loginWithCode') }
  </action>

  <action name="afterCodeLogin">
    codeSentTo = '';
    codeEntry = LoginCodeInput();
    refresh('me');
    returnIfAsked()
  </action>

  <action name="askWithAnotherAddress">
    codeSentTo = '';
    codeEntry = LoginCodeInput()
  </action>

  <const name="ui.pageBackground" value="{canvas}"/>
  <const name="ui.text"           value="{ink}"/>
  <const name="ui.surface"        value="{paper}"/>
  <const name="ui.onSurface"      value="{ink}"/>
  <const name="ui.primary"        value="{brand}"/>
  <const name="ui.onPrimary"      value="{onBrand}"/>
  <const name="ui.accent"         value="{accent}"/>
  <const name="ui.onAccent"       value="{onAccent}"/>
  <const name="ui.danger"         value="{danger}"/>
  <const name="ui.onDanger"       value="{onDanger}"/>
  <const name="ui.border"         value="{line}"/>
  <const name="ui.outline"        value="{dim}"/>
  <const name="ui.track"          value="{track}"/>

  <style name="page" bg="{ui.pageBackground}" textColor="{ink}" grow="1"/>
  <style name="sheet" pad="24" gap="16" maxWidth="720" width="100%" selfAlign="center"/>
  <style name="form" pad="24" gap="16" maxWidth="420" width="100%" selfAlign="center"/>
  <style name="h1" size="26" weight="700"/>
  <style name="h2" size="16" weight="700"/>
  <style name="dim" size="14" textColor="{dim}"/>
  <style name="err" size="13" textColor="{ui.danger}"/>
  <style name="card" bg="{ui.surface}" radius="12" pad="16" gap="12" border="1 {ui.border}"/>

  <!-- A form's submit affordance cannot be a `kit` component: `submit` binds to the LEXICALLY
       enclosing form (§10.8), and a component's own <button> is lexically inside `kit`. So the
       primary action is a bare button wearing kit's metrics; navigation stays <Button>. -->
  <style name="btnPrimary" bg="{ui.primary}" textColor="{ui.onPrimary}"
         radius="9" pad="10 16" size="14" weight="700" align="center">
    <pressed scale="0.97"/>
    <disabled opacity="0.5"/>
    <transition props="scale" duration="0.12" ease="out"/>
  </style>

  <style name="topbar" bg="{ui.surface}" pad="10 24" gap="6" border="0 0 1 0 {ui.border}" align="center"/>
  <style name="brand" size="15" weight="700" pad="0 10 0 0"/>
  <style name="tab" pad="8 12" radius="8" size="14" weight="600" textColor="{dim}" bg="{ui.surface}"/>
  <style name="tabOn" textColor="{ink}" bg="{ui.pageBackground}"/>
  <style name="pill" size="12" weight="700" textColor="{ink}" bg="{ui.track}" radius="6" pad="3 8"/>
  <style name="cardLink" bg="{ui.surface}" radius="12" pad="16" gap="10" border="1 {ui.border}"/>

  <!-- ── the four sections ───────────────────────────────────────
       Play, Manage, Store and Account, always all four (hub §1.2): Manage is there for a user who
       manages nothing but their personal organization, because every user has one. Three of
       them are lazy files; the catalog's cards they share with the signed-out root are parts'. -->

  <screen name="app" title="{t('appName')}">
    <routes>
      <route path="" screen="play" src="./play.kaf"/>
      <route path="manage" screen="manage" src="./manage.kaf"/>
      <route path="store" screen="store" src="./store.kaf"/>
      <route path="account" screen="account"/>
    </routes>

    <column style="page">
      <row style="topbar">
        <text style="brand">{t('appName')}</text>
        <button style="tab" link="/" link:active="tabOn" label="{t('play')}"/>
        <button style="tab" link="/manage" link:active="tabOn" link:active:match="in" label="{t('manage')}"/>
        <button style="tab" link="/store" link:active="tabOn" link:active:match="in" label="{t('store')}"/>
        <button style="tab" link="/account" link:active="tabOn" link:active:match="in" label="{t('account')}"/>
      </row>
      <outlet/>
    </column>
  </screen>

  <!-- ── the catalog, signed out ─────────────────────────────────
       The root for a visitor with no session (hub §2.1): the published programs, with sign-in and
       sign-up in the header. A signed-in visitor who lands here by URL gets a way back in. -->

  <screen name="catalog" title="{t('catalogTitle')}">
    <rpc name="programs" call="CatalogService.list" auto/>
    <column style="page">
      <row style="topbar">
        <text style="brand">{t('appName')}</text>
        <spacer/>
        <match>
          <case test="{wasSignedIn}">
            <Button label="{t('play')}" tone="quiet" link="/"/>
          </case>
          <else>
            <Button label="{t('signIn')}" tone="quiet" link="/login"/>
            <Button label="{t('createAccount')}" link="/signup"/>
          </else>
        </match>
      </row>
      <column style="sheet">
        <text style="h1">{t('catalogHeading')}</text>
        <text style="dim">{t('catalogBody')}</text>
        <await from="{programs}" as="rows">
          <loading><column style="card"><skeleton lines="4"/></column></loading>
          <error>
            <column style="card">
              <text style="err">{t('serviceOffline')}</text>
              <text style="dim">{programs.error.code}</text>
            </column>
          </error>
          <CatalogList programs="{rows}"/>
        </await>
      </column>
    </column>
  </screen>


  <!-- ── the join page ───────────────────────────────────────────
       Names the calendar, its organization, its dates and its open time, and has one action
       (invitations §6.1). A visitor who cannot join is told why (§6.2); a dead link is *not found*.
       Signed out, the page offers sign-in and sign-up and remembers itself as the way back. -->

  <component name="JoinCard">
    <param name="p"/>
    <column style="card">
      <text style="h1">{p.name}</text>
      <if test="{p.organizationName != null}"><text style="dim">{p.organizationName}</text></if>
      <row gap="8" wrap>
        <text style="pill">{p.startDate} → {p.endDate}</text>
        <text style="pill">{t('opensDaily', { time: fmt(date.fromIsoTime(p.openTime), 'time') })}</text>
        <text style="pill">{p.timezone}</text>
      </row>
    </column>
  </component>

  <screen name="join" params="token" title="{t('joinTitle')}">
    <rpc name="page" call="JoinService.look" args="{{ key: { token: token } }}" auto/>
    <rpc name="join" call="JoinService.join" args="{{ key: { token: token } }}"
         on:done="nav.to(str.concat('/play/', join.data.calendarId))"/>
    <action name="signInFirst">
      returnTo = str.concat('/join/', token);
      nav.to('/login')
    </action>
    <action name="signUpFirst">
      returnTo = str.concat('/join/', token);
      nav.to('/signup')
    </action>
    <column style="page">
      <row style="topbar">
        <text style="brand">{t('appName')}</text>
        <spacer/>
        <if test="{wasSignedIn}"><Button label="{t('play')}" tone="quiet" link="/"/></if>
      </row>
      <column style="sheet">
        <await from="{page}" as="p">
          <loading><column style="card"><skeleton lines="4"/></column></loading>
          <error>
            <column style="card">
              <text style="h1">{t('joinNotFound')}</text>
              <text style="dim">{t('joinNotFoundBody')}</text>
            </column>
          </error>
          <JoinCard p="{p}"/>
          <column style="card">
            <match>
              <case test="{!wasSignedIn}">
                <text style="dim">{t('joinSignInFirst')}</text>
                <row gap="10" wrap>
                  <Button label="{t('signIn')}" on:tap="signInFirst()"/>
                  <Button label="{t('createAccount')}" tone="quiet" on:tap="signUpFirst()"/>
                </row>
              </case>
              <case test="{p.joined}">
                <text style="dim">{t('joinAlready')}</text>
                <row><Button label="{t('goToCalendar')}" link="/play/{p.calendarId}"/></row>
              </case>
              <case test="{p.reason == 'full'}"><text style="err">{t('joinFull')}</text></case>
              <case test="{p.reason == 'ended'}"><text style="err">{t('joinEnded')}</text></case>
              <case test="{p.reason == 'domain'}"><text style="err">{t('joinWrongDomain')}</text></case>
              <else>
                <text style="dim">{t('joinBody')}</text>
                <if test="{join.error != null}">
                  <text style="err">{list.len(join.error.details ?? []) > 0 ? join.error.details[0].message : t('serviceOffline')}</text>
                </if>
                <row><Button label="{join.loading ? t('joining') : t('joinAction')}" disabled="{join.loading}" on:tap="send('join')"/></row>
              </else>
            </match>
          </column>
        </await>
      </column>
    </column>
  </screen>

  <!-- The personal link (invitations §2.3, §8.2): the page names the address it belongs to, so a
       visitor with no account signs up with it filled in; an account at another address is told
       the invitation is someone else's and may sign out to switch. -->
  <screen name="joinInvited" params="invitation" title="{t('joinTitle')}">
    <rpc name="page" call="JoinService.look" args="{{ key: { invitation: invitation } }}" auto/>
    <rpc name="join" call="JoinService.join" args="{{ key: { invitation: invitation } }}"
         on:done="nav.to(str.concat('/play/', join.data.calendarId))"/>
    <action name="signInFirst">
      returnTo = str.concat('/join/i/', invitation);
      nav.to('/login')
    </action>
    <action name="signUpFirst">
      returnTo = str.concat('/join/i/', invitation);
      account.email = page.data?.email ?? '';
      nav.to('/signup')
    </action>
    <action name="switchAccount">
      returnTo = str.concat('/join/i/', invitation);
      send('logout')
    </action>
    <column style="page">
      <row style="topbar">
        <text style="brand">{t('appName')}</text>
        <spacer/>
        <if test="{wasSignedIn}"><Button label="{t('play')}" tone="quiet" link="/"/></if>
      </row>
      <column style="sheet">
        <await from="{page}" as="p">
          <loading><column style="card"><skeleton lines="4"/></column></loading>
          <error>
            <column style="card">
              <text style="h1">{t('joinNotFound')}</text>
              <text style="dim">{t('joinNotFoundBody')}</text>
            </column>
          </error>
          <JoinCard p="{p}"/>
          <column style="card">
            <match>
              <case test="{!wasSignedIn}">
                <text style="dim">{t('joinInvitedFor', { email: p.email ?? '' })}</text>
                <row gap="10" wrap>
                  <Button label="{t('signIn')}" on:tap="signInFirst()"/>
                  <Button label="{t('createAccount')}" tone="quiet" on:tap="signUpFirst()"/>
                </row>
              </case>
              <case test="{p.joined}">
                <text style="dim">{t('joinAlready')}</text>
                <row><Button label="{t('goToCalendar')}" link="/play/{p.calendarId}"/></row>
              </case>
              <case test="{p.reason == 'other'}">
                <text style="err">{t('joinOtherAddress', { email: p.email ?? '' })}</text>
                <row><Button label="{logout.loading ? t('signingOut') : t('switchAccount')}" tone="quiet" disabled="{logout.loading}" on:tap="switchAccount()"/></row>
              </case>
              <case test="{p.reason == 'full'}"><text style="err">{t('joinFull')}</text></case>
              <case test="{p.reason == 'ended'}"><text style="err">{t('joinEnded')}</text></case>
              <else>
                <text style="dim">{t('joinBody')}</text>
                <if test="{join.error != null}">
                  <text style="err">{list.len(join.error.details ?? []) > 0 ? join.error.details[0].message : t('serviceOffline')}</text>
                </if>
                <row><Button label="{join.loading ? t('joining') : t('joinAction')}" disabled="{join.loading}" on:tap="send('join')"/></row>
              </else>
            </match>
          </column>
        </await>
      </column>
    </column>
  </screen>

  <!-- The manager invite's accept page (roles §4.4–§4.5, hub §4.2): who asks and for which
       organization; accepting lands Manage on it. Bound to the address like the personal link. -->
  <screen name="acceptInvite" params="token" title="{t('acceptTitle')}">
    <rpc name="page" call="OrganizationService.lookInvite" args="{{ token: token }}" auto/>
    <rpc name="accept" call="OrganizationService.acceptInvite" args="{{ token: token }}"
         on:done="manageOrg = accept.data.id; nav.to('/manage')"/>
    <action name="signInFirst">
      returnTo = str.concat('/invite/', token);
      nav.to('/login')
    </action>
    <action name="signUpFirst">
      returnTo = str.concat('/invite/', token);
      account.email = page.data?.email ?? '';
      nav.to('/signup')
    </action>
    <action name="switchAccount">
      returnTo = str.concat('/invite/', token);
      send('logout')
    </action>
    <column style="page">
      <row style="topbar">
        <text style="brand">{t('appName')}</text>
        <spacer/>
        <if test="{wasSignedIn}"><Button label="{t('play')}" tone="quiet" link="/"/></if>
      </row>
      <column style="sheet">
        <await from="{page}" as="p">
          <loading><column style="card"><skeleton lines="4"/></column></loading>
          <error>
            <column style="card">
              <text style="h1">{t('joinNotFound')}</text>
              <text style="dim">{t('acceptNotFoundBody')}</text>
            </column>
          </error>
          <column style="card">
            <text style="h1">{t('acceptHeading', { organization: p.organizationName })}</text>
            <text style="dim">{t('acceptBody', { inviter: p.inviterName })}</text>
            <text style="dim">{t('managerCan')}</text>
          </column>
          <column style="card">
            <match>
              <case test="{!wasSignedIn}">
                <text style="dim">{t('joinInvitedFor', { email: p.email })}</text>
                <row gap="10" wrap>
                  <Button label="{t('signIn')}" on:tap="signInFirst()"/>
                  <Button label="{t('createAccount')}" tone="quiet" on:tap="signUpFirst()"/>
                </row>
              </case>
              <case test="{p.manages}">
                <text style="dim">{t('acceptAlready')}</text>
                <row><Button label="{t('goToManage')}" link="/manage"/></row>
              </case>
              <case test="{p.reason == 'other'}">
                <text style="err">{t('joinOtherAddress', { email: p.email })}</text>
                <row><Button label="{logout.loading ? t('signingOut') : t('switchAccount')}" tone="quiet" disabled="{logout.loading}" on:tap="switchAccount()"/></row>
              </case>
              <else>
                <if test="{accept.error != null}">
                  <text style="err">{list.len(accept.error.details ?? []) > 0 ? accept.error.details[0].message : t('serviceOffline')}</text>
                </if>
                <row><Button label="{accept.loading ? t('accepting') : t('acceptAction')}" disabled="{accept.loading}" on:tap="send('accept')"/></row>
              </else>
            </match>
          </column>
        </await>
      </column>
    </column>
  </screen>

  <screen name="joinAsManager" params="calendarId" title="{t('joinTitle')}">
    <rpc name="page" call="JoinService.look" args="{{ key: { calendarId: calendarId } }}" auto/>
    <rpc name="join" call="JoinService.join" args="{{ key: { calendarId: calendarId } }}"
         on:done="nav.to(str.concat('/play/', join.data.calendarId))"/>
    <column style="page">
      <row style="topbar">
        <text style="brand">{t('appName')}</text>
        <Button label="{t('backToManage')}" tone="quiet" link="/manage/calendars/{calendarId}"/>
        <spacer/>
        <if test="{wasSignedIn}"><Button label="{t('play')}" tone="quiet" link="/"/></if>
      </row>
      <column style="sheet">
        <await from="{page}" as="p">
          <loading><column style="card"><skeleton lines="4"/></column></loading>
          <error>
            <column style="card">
              <text style="h1">{t('joinNotFound')}</text>
              <text style="dim">{t('joinNotFoundBody')}</text>
            </column>
          </error>
          <JoinCard p="{p}"/>
          <column style="card">
            <match>
              <case test="{p.joined}">
                <text style="dim">{t('joinAlready')}</text>
                <row><Button label="{t('goToCalendar')}" link="/play/{p.calendarId}"/></row>
              </case>
              <case test="{p.reason == 'full'}"><text style="err">{t('joinFull')}</text></case>
              <case test="{p.reason == 'ended'}"><text style="err">{t('joinEnded')}</text></case>
              <else>
                <text style="dim">{t('joinAsManagerBody')}</text>
                <if test="{join.error != null}">
                  <text style="err">{list.len(join.error.details ?? []) > 0 ? join.error.details[0].message : t('serviceOffline')}</text>
                </if>
                <row><Button label="{join.loading ? t('joining') : t('joinAction')}" disabled="{join.loading}" on:tap="send('join')"/></row>
              </else>
            </match>
          </column>
        </await>
      </column>
    </column>
  </screen>

  <!-- ── a program's page ────────────────────────────────────────
       The catalog view (programs §5.1), public like the catalog, with the tiers beside it (store
       §2.3): a signed-in reader sees the free tier first, since every user has a personal
       organization, and takes it in one action (§3.8) — or is told it is already held (§9.7). A
       program with no listed product says not for sale (§2.2). A paid tier's button opens the
       checkout page (§2.4); a visitor is sent to sign in and brought back to this page. -->

  <screen name="program" params="id" title="{t('programTitle')}">
    <rpc name="page" call="CatalogService.get" args="{{ id: id }}" auto/>
    <rpc name="orgs" call="OrganizationService.mine" auto="{wasSignedIn}"/>
    <derived name="personalId" expr="{list.find(orgs.data ?? [], o => o.personal)?.id ?? ''}"/>
    <rpc name="held" call="StoreService.placeable" args="{{ organizationId: personalId }}" auto="{wasSignedIn && personalId != ''}"/>
    <derived name="heldFree" expr="{list.exists(held.data ?? [], h => h.programId == id && h.free)}"/>
    <rpc name="takeFree" call="StoreService.takeFree" args="{{ input: { programId: id, organizationId: personalId } }}"
         on:done="manageOrg = ''; nav.to('/manage')"/>
    <action name="signInToBuy">
      returnTo = str.concat('/programs/', id);
      nav.to('/login')
    </action>
    <column style="page">
      <row style="topbar">
        <text style="brand">{t('appName')}</text>
        <Button label="{t('backToCatalog')}" tone="quiet" link="{wasSignedIn ? '/store' : '/catalog'}"/>
        <spacer/>
        <match>
          <case test="{wasSignedIn}">
            <Button label="{t('play')}" tone="quiet" link="/"/>
          </case>
          <else>
            <Button label="{t('signIn')}" tone="quiet" link="/login"/>
            <Button label="{t('createAccount')}" link="/signup"/>
          </else>
        </match>
      </row>
      <column style="sheet">
        <await from="{page}" as="v">
          <loading><column style="card"><skeleton lines="5"/></column></loading>
          <error>
            <column style="card">
              <text style="err">{t('serviceOffline')}</text>
              <text style="dim">{page.error.code}</text>
            </column>
          </error>
          <match>
            <case test="{v.program == null}">
              <text style="h1">{t('programNotFound')}</text>
            </case>
            <else>
              <column style="card">
                <ProgramFace title="{v.program.title[lang] ?? v.program.title.en ?? v.program.title.fr}"
                             description="{v.program.description[lang] ?? v.program.description.en ?? v.program.description.fr}"
                             lengthLabel="{t('dayCount', { n: v.program.length })}"
                             days="{v.program.days}" typeCounts="{v.program.typeCounts}" labels="{typeLabels}"
                             background="{v.program.background}"/>
              </column>
              <match>
                <case test="{!v.program.forSale}">
                  <column style="card">
                    <text style="h2">{t('notForSale')}</text>
                    <text style="dim">{t('notForSaleBody')}</text>
                  </column>
                </case>
                <else>
                  <column style="card">
                    <text style="h2">{t('tiers')}</text>
                    <if test="{wasSignedIn}">
                      <each in="{list.filter(v.program.tiers, x => x.price == 0)}" as="tier" key="{tier.productId}">
                        <row gap="10" align="center" wrap>
                          <column grow="1">
                            <text style="h2">{t('freeTier')}</text>
                            <text style="dim">{t('capLabel', { n: tier.cap })}</text>
                          </column>
                          <match>
                            <case test="{heldFree}">
                              <Button label="{t('alreadyHeld')}" tone="quiet" link="/manage"/>
                            </case>
                            <else>
                              <Button label="{takeFree.loading ? t('taking') : t('takeFreeTier')}"
                                      disabled="{takeFree.loading || personalId == ''}" on:tap="send('takeFree')"/>
                            </else>
                          </match>
                        </row>
                      </each>
                    </if>
                    <each in="{list.filter(v.program.tiers, x => x.price > 0)}" as="tier" key="{tier.productId}">
                      <row gap="10" align="center" wrap>
                        <column grow="1">
                          <text style="h2">{fmt(tier.price / 100, 'currency', 'CAD')}</text>
                          <text style="dim">{t('capLabel', { n: tier.cap })} · {t('beforeTax')}</text>
                        </column>
                        <if test="{wasSignedIn}">
                          <Button label="{t('buyTier')}" link="/store/checkout/{id}/{tier.productId}"/>
                        </if>
                      </row>
                    </each>
                    <if test="{!wasSignedIn}">
                      <text style="dim">{t('signInToBuy')}</text>
                      <row><Button label="{t('signIn')}" on:tap="signInToBuy()"/></row>
                    </if>
                    <if test="{takeFree.error != null}">
                      <text style="err">{list.len(takeFree.error.details ?? []) > 0 ? takeFree.error.details[0].message : t('serviceOffline')}</text>
                    </if>
                  </column>
                </else>
              </match>
            </else>
          </match>
        </await>
      </column>
    </column>
  </screen>

  <!-- ── signing in ──────────────────────────────────────────────
       One door, two states on one route: the address, then the six digits. What makes the code
       safe is invisible here — it is bound to a cookie this document cannot read, so it spends
       in this browser and in no other. The sent state says the same sentence to everyone,
       because the host answered the same to everyone. -->

  <screen name="login" title="{t('signInTitle')}">
    <column style="page">
      <column style="form">
        <text style="h1">{t('signInHeading')}</text>
        <match>
          <case test="{codeSentTo != ''}">
            <text style="dim">{t('codeSentBody', { email: codeSentTo })}</text>
            <form name="codeForm" on:submit="submitLoginCode()">
              <column style="card">
                <text-input bind:value="codeEntry.code" label="{t('code')}" autofocus/>
                <match>
                  <case test="{loginWithCode.error != null && loginWithCode.error.code == 'VALIDATION_ERROR' && !codeEntry.valid}">
                    <text style="err">{t('checkTheFields')}</text>
                  </case>
                  <case test="{loginWithCode.error != null && loginWithCode.error.code != 'VALIDATION_ERROR'}">
                    <text style="err">{t('serviceOffline')}</text>
                  </case>
                </match>
                <row justify="end">
                  <button submit style="btnPrimary"
                          label="{loginWithCode.loading ? t('signingIn') : t('signIn')}"
                          disabled="{loginWithCode.loading || (codeForm.attempted && !codeEntry.valid)}"/>
                </row>
              </column>
            </form>
            <text style="dim">{t('nothingSentIfUnknown')}</text>
            <row justify="center" gap="10" wrap>
              <Button label="{t('useAnotherAddress')}" tone="quiet" on:tap="askWithAnotherAddress()"/>
              <Button label="{t('noAccountYet')}" tone="quiet" link="/signup"/>
            </row>
          </case>
          <else>
            <text style="dim">{t('codeBody')}</text>
            <form name="codeAskForm" on:submit="askForCode()">
              <column style="card">
                <text-input bind:value="codeRequest.email" label="{t('email')}" kind="email" autofocus/>
                <match>
                  <case test="{requestLoginCode.error != null && requestLoginCode.error.code == 'VALIDATION_ERROR' && !codeRequest.valid}">
                    <text style="err">{t('checkTheFields')}</text>
                  </case>
                  <case test="{requestLoginCode.error != null && requestLoginCode.error.code != 'VALIDATION_ERROR'}">
                    <text style="err">{t('serviceOffline')}</text>
                  </case>
                </match>
                <row justify="end">
                  <button submit style="btnPrimary"
                          label="{requestLoginCode.loading ? t('sending') : t('sendTheCode')}"
                          disabled="{requestLoginCode.loading || (codeAskForm.attempted && !codeRequest.valid)}"/>
                </row>
              </column>
            </form>
            <row justify="center">
              <Button label="{t('noAccountYet')}" tone="quiet" link="/signup"/>
            </row>
          </else>
        </match>
      </column>
    </column>
  </screen>

  <screen name="signup" title="{t('signUpTitle')}">
    <column style="page">
      <column style="form">
        <text style="h1">{t('signUpHeading')}</text>
        <form name="signUpForm" on:submit="createAccount()">
          <column style="card">
            <text-input bind:value="account.displayName" label="{t('displayName')}" autofocus/>
            <text-input bind:value="account.email" label="{t('email')}" kind="email"/>
            <text style="dim">{t('signUpNote')}</text>
            <match>
              <case test="{signup.error != null && signup.error.code == 'VALIDATION_ERROR' && !account.valid}">
                <text style="err">{t('checkTheFields')}</text>
              </case>
              <case test="{signup.error != null && signup.error.code != 'VALIDATION_ERROR'}">
                <text style="err">{t('serviceOffline')}</text>
              </case>
            </match>
            <row justify="end">
              <button submit style="btnPrimary"
                      label="{signup.loading ? t('creatingAccount') : t('createAccount')}"
                      disabled="{signup.loading || (signUpForm.attempted && !account.valid)}"/>
            </row>
          </column>
        </form>
        <row justify="center">
          <Button label="{t('haveAnAccount')}" tone="quiet" link="/login"/>
        </row>
      </column>
    </column>
  </screen>

  <!-- The profile (hub §9.1): the name, the address, and the picture (media §1.4) — set and
       replaced here and nowhere else. The upload chain is the screen's (docs/media.md): the
       ticket for the `picture` slot, the bytes to the store, the confirm, then `setPicture` on
       the row and `me` re-read; Remove clears the slot the same way. A row still processing is
       re-read on a clock until it is not. -->
  <screen name="account" title="{t('accountTitle')}">
    <state name="pictureFile" value="{null}"/>
    <state name="pictureInput" struct="SetPictureInput"/>
    <rpc name="pictureTicket" call="MediaService.ticket" args="{{ input: { slot: 'picture', type: pictureFile?.type ?? '', size: pictureFile?.size ?? 0 } }}" on:done="send('picturePut')"/>
    <fetch name="picturePut" method="post" url="{pictureTicket.data?.url ?? ''}" body="{pictureTicket.data?.fields}" upload="{pictureFile}" on:done="send('pictureConfirm')"/>
    <rpc name="pictureConfirm" call="MediaService.confirm" args="{{ ticketId: pictureTicket.data?.id ?? '' }}" on:done="pictureInput.mediaId = pictureConfirm.data.mediaId; send('setPicture')"/>
    <rpc name="setPicture" call="AccountService.setPicture" args="{{ input: pictureInput }}" on:done="pictureFile = null; refresh('me')"/>
    <derived name="pictureBusy" expr="{pictureTicket.loading || picturePut.loading || pictureConfirm.loading || setPicture.loading}"/>
    <derived name="pictureError" expr="{pictureTicket.error ?? pictureConfirm.error ?? setPicture.error}"/>
    <derived name="pictureNote" expr="{picturePut.error != null ? t('uploadFailed') : pictureError != null ? (list.len(pictureError.details ?? []) > 0 ? pictureError.details[0].message : t('serviceOffline')) : ''}"/>
    <action name="pickPicture" params="file">
      pictureFile = file;
      send('pictureTicket')
    </action>
    <action name="clearPicture">
      pictureFile = null;
      pictureInput = SetPictureInput();
      send('setPicture')
    </action>
    <timer name="pictureWatch" every="3" running="{me.data?.user?.picture?.state == 'pending'}" do="refresh('me')"/>

    <column style="sheet">
      <text style="h1">{t('accountHeading')}</text>
      <await from="{me}" as="s">
        <loading><column style="card"><skeleton lines="3"/></column></loading>
        <error>
          <column style="card">
            <text style="err">{t('serviceOffline')}</text>
            <text style="dim">{me.error.code}</text>
          </column>
        </error>
        <column style="card">
          <row gap="12" align="center">
            <Avatar name="{s.user?.displayName ?? ''}" picture="{s.user?.picture}" size="{56}"/>
            <column grow="1" gap="2">
              <text style="h2">{s.user?.displayName}</text>
              <text style="dim">{s.user?.email}</text>
              <text style="dim">{t('accountSince', { on: fmt(s.user?.createdOn, 'date-year') })}</text>
              <text style="dim">{t('accountLanguage', { tag: s.user?.locale })}</text>
            </column>
          </row>
        </column>
        <column style="card">
          <text style="h2">{t('pictureHeading')}</text>
          <text style="dim">{t('pictureBody')}</text>
          <Upload ref="{s.user?.picture}" label="{t('pictureLabel')}" progress="{picturePut.progress}" busy="{pictureBusy}" error="{pictureNote}"
                  on:pick="pickPicture(event.file)" on:clear="clearPicture()"/>
        </column>
      </await>
      <row gap="10" wrap>
        <Button label="{logout.loading ? t('signingOut') : t('signOut')}" tone="quiet"
                disabled="{logout.loading}" on:tap="send('logout')"/>
      </row>
      <!-- What a person does who suspects a browser they no longer hold is still signed in
           (accounts §3.7): every session ends, this one included. -->
      <column style="card">
        <text style="h2">{t('signOutEverywhere')}</text>
        <text style="dim">{t('signOutEverywhereBody')}</text>
        <row>
          <Button label="{logoutEverywhere.loading ? t('signingOut') : t('signOutEverywhere')}" tone="quiet"
                  disabled="{logoutEverywhere.loading}" on:tap="send('logoutEverywhere')"/>
        </row>
      </column>
    </column>
  </screen>

  <!-- ── confirmation ────────────────────────────────────────────
       A code, not a link: the visitor never left this tab, and six digits are unspendable by an
       unfurler, a prefetcher or a mail-security rewriter. -->

  <screen name="confirm" title="{t('confirmTitle')}">
    <column style="page">
      <column style="form">
        <text style="h1">{t('confirmHeading')}</text>
        <text style="dim">{t('confirmBody', { email: pendingEmail })}</text>
        <form name="confirmForm" on:submit="submitCode()">
          <column style="card">
            <text-input bind:value="confirmation.code" label="{t('code')}" autofocus/>
            <match>
              <case test="{confirmSignup.error != null && confirmSignup.error.code == 'VALIDATION_ERROR' && !confirmation.valid}">
                <text style="err">{t('checkTheFields')}</text>
              </case>
              <case test="{confirmSignup.error != null && confirmSignup.error.code != 'VALIDATION_ERROR'}">
                <text style="err">{t('serviceOffline')}</text>
              </case>
            </match>
            <row justify="end">
              <button submit style="btnPrimary"
                      label="{confirmSignup.loading ? t('confirming') : t('confirmAction')}"
                      disabled="{confirmSignup.loading || (confirmForm.attempted && !confirmation.valid)}"/>
            </row>
          </column>
        </form>
        <row justify="center">
          <Button label="{t('startOver')}" tone="quiet" link="/signup"/>
        </row>
      </column>
    </column>
  </screen>

  <screen name="lost" title="{t('lostTitle')}">
    <column style="page">
      <column style="sheet">
        <text style="h1">{t('lostHeading')}</text>
        <text style="dim">{t('lostBody')}</text>
        <row>
          <Button label="{t('backToStart')}" link="/"/>
        </row>
      </column>
    </column>
  </screen>

</kaf>
