$ storebound console plan play-content-rating
Play Console: Content rating (IARC)
start: https://play.google.com/console
  then: Pick the developer account if asked (set play.developerId for a deep link)
  then: the app (com.example.prismpuddle) in the app list
  then: Policy and programs > App content
  then: Content rating > Start (or Manage)

Steps:
  1. Open App content > Content ratings (/app-content/content-rating-overview), then "Start questionnaire": it opens /app-content/content-rating-iarc-questionnaire.  [click: "Start questionnaire"]
  2. Page 1: type the email address and pick the category (material radio: JS click on the inner [role=radio]). The IARC Terms of Use checkbox is an agreement: never tick it; the owner ticks it (it is in the batch). Then Next.  [click: "Next"]
  3. The questionnaire is ONE long page of sections (Violence, Fear, Sexuality, Gambling, Language, Controlled Substance, Crude Humor, Digital Purchases, Miscellaneous). Answer each section's Yes / No as the sheet says; each Yes reveals follow-ups right below it, so answer top to bottom and re-read the page after every Yes. Run it as one in-page job (plan.inPageLoop) and poll window.__log.
  4. Violence follow-ups (after Yes): the humans / non-humans checkboxes, then setting, childlike style, reactions, presentation checkboxes, blood level, historical war setting, innocent characters, fierce sounds.
  5. Fear, Sexuality, Gambling, Language, Controlled Substance, Crude Humor follow-ups (after Yes): answer the questions shown as the sheet says (details.<section>.other).
  6. Digital Purchases follow-ups (after "purchases of digital goods" Yes): the random items (loot box) question, then trading between players.
  7. Miscellaneous: user interaction (after Yes: block, report, moderation, friends only), location sharing, Nazi symbols, Korea history, terrorism, crimes.
  8. Press Save (stores the draft; nothing goes to IARC yet), then Next: the Summary shows the rating per authority (PEGI, ESRB, USK, ClassInd, Google Play ...).  [click: "Save", "Next"]
  9. Summary: stop. Do NOT press its Save (the final button here is "Save", not "Submit"): tell the owner the ratings shown and let them press Save (it applies the IARC ratings).
  10. Verify: read the Summary page as text and pipe it to `storebound console verify play-content-rating --from-text -`.

Answer sheet:
  [ok]    Email address: s***@example.com  -> type "Email address"  @ Email address and category
  [ask]   Category: (you decide)  -> pick "Category"  @ Email address and category
           Pick the category; set console.contentRating.category to have it filled.
  [you]   IARC Terms of Use: the owner ticks it (an agreement)  -> untick "IARC Terms of Use"  @ Email address and category
           An agreement on page 1 (a checkbox): only the owner ticks it, never the agent or storebound.
  [ask]   Violence: (you decide)  -> pick "Does the app contain (any )violen"  @ Questionnaire (Does the app contain (any )violen)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.violence.humans, violence.nonHumans, violence.setting, violence.childlike, violence.reactions, violence.presentation, violence.blood, violence.historicalWar, violence.innocents, violence.fierceSounds too.
  [ask]   Fear / horror: (you decide)  -> pick "Does the app contain …scary"  @ Questionnaire (Does the app contain …scary)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.fear.other too.
  [ask]   Sexuality or nudity: (you decide)  -> pick "Does the app contain …sexual"  @ Questionnaire (Does the app contain …sexual)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.sexuality.other too.
  [ask]   Language (profanity): (you decide)  -> pick "Does the app contain …profan"  @ Questionnaire (Does the app contain …profan)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.language.other too.
  [ask]   Controlled substances (drugs, alcohol, tobacco): (you decide)  -> pick "Does the app contain …drug"  @ Questionnaire (Does the app contain …drug)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.controlledSubstances.other too.
  [ask]   Crude humour: (you decide)  -> pick "crude humour"  @ Questionnaire (crude humour)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.crudeHumor.other too.
  [ask]   Gambling with real money: (you decide)  -> pick "Does the app allow …gambl…real money"  @ Questionnaire (Does the app allow …gambl…real money)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.
  [ask]   Simulated gambling: (you decide)  -> pick "simulated gambling"  @ Questionnaire (simulated gambling)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.
  [ok]    Random paid items (loot boxes): No  -> pick "No"  @ Questionnaire (random (paid )items)
  [ask]   Users can interact or exchange content: (you decide)  -> pick "users …interact"  @ Questionnaire (users …interact)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.userInteraction.block, userInteraction.report, userInteraction.moderation, userInteraction.friendsOnly too.
  [ask]   Shares the user's location with other users: (you decide)  -> pick "share …(physical )location"  @ Questionnaire (share …(physical )location)
           Answer from the app's real content; set console.contentRating.answers to have it filled.
  [ask]   Digital purchases: (you decide)  -> pick "digital goods"  @ Questionnaire (digital goods)
           Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.digitalPurchases.playerTrading too.
  [ask]   Unrestricted internet access (browser): (you decide)  -> pick "unrestricted internet"  @ Questionnaire (unrestricted internet)
           Answer from the app's real content; set console.contentRating.answers to have it filled.
  [ask]   Miscellaneous: Nazi symbols or references?: (you decide)  -> pick "nazi"  @ Questionnaire: Miscellaneous follow-ups (nazi)
           Answer from the app's real content; set console.contentRating.details.miscellaneous.naziSymbols to have it filled.
  [ask]   Miscellaneous: content about Korean history that may be sensitive?: (you decide)  -> pick "korea"  @ Questionnaire: Miscellaneous follow-ups (korea)
           Answer from the app's real content; set console.contentRating.details.miscellaneous.koreaHistory to have it filled.
  [ask]   Miscellaneous: terrorism or extremist content?: (you decide)  -> pick "terror"  @ Questionnaire: Miscellaneous follow-ups (terror)
           Answer from the app's real content; set console.contentRating.details.miscellaneous.terrorism to have it filled.
  [ask]   Miscellaneous: depicts or encourages crimes?: (you decide)  -> pick "crime"  @ Questionnaire: Miscellaneous follow-ups (crime)
           Answer from the app's real content; set console.contentRating.details.miscellaneous.crimes to have it filled.

Never press: "IARC Terms of Use box", "Save (Summary page)", "Publish", "Submit", "Create", "Create app", "Send", "Release", "Definitions and Examples", "?", "More information"
Final button (the owner's): Save (Summary page)
Rule: Press only navigation (Next, Back, Get Started, Edit, Set Up, Start / Manage links) and draft saves (Save draft / Save (questionnaire page)). Never press Save (Summary page) or anything that publishes, submits, creates, sends or accepts; stop and tell the owner.

UI facts:
  - Verified 2026-10-12 (an agent with a browser tool on a real game):
  - App content > Content ratings is /app-content/content-rating-overview; "Start questionnaire" opens /app-content/content-rating-iarc-questionnaire.
  - Page 1: email address, the category radio, and an IARC Terms of Use checkbox: an agreement, the owner ticks it, never the agent.
  - The questionnaire is one long page of sections; each Yes reveals follow-ups (Violence: humans / non-humans checkboxes, then setting, childlike style, reactions, presentation checkboxes, blood level, historical war setting, innocent characters, fierce sounds; Digital Purchases: digital goods, then loot boxes, then player trading; Miscellaneous: user interaction then block / report / moderation / friends-only, location sharing, Nazi symbols, Korea history, terrorism, crimes). Follow-up wording here is storebound's: match by topic and the console's own labels.
  - Save on the questionnaire stores it; Next goes to the Summary with per-authority ratings. The Summary's final button is "Save" (not "Submit"): the owner's.
  - Example result (the game of that run): PEGI 7, ESRB E10+, USK 12, ClassInd 18 (paid random items alone made it 18), Google Play 7+.
  - The email address field gets the full app.contactEmail from the config (printed masked here).
  - Play Console radios and checkboxes are <material-radio> / <material-checkbox>: computer-tool clicks and setting the native input (form_input) can leave the Angular model unchanged. Click the inner [role=radio] / [role=checkbox] (or the <input> itself) with a JS el.click(), then read aria-checked (or input.checked). Their labels sit in sibling nodes, not in an aria-label: walk up from the control to the nearest parent whose innerText is not empty and match by "starts with" (verified 2026-10-12).
  - An app's Play Console home is /console/u/0/developers/<developerId>/app/<appId>/app-dashboard (verified 2026-10-12); with play.appId (or the id storebound remembered) the plan starts there.

Driving:
  - Use your own browser tool (Claude in Chrome, a built-in browser, Playwright). Read the page as text or an accessibility tree first; click by visible text; adapt when the page differs from these steps (the console changes without notice), but never press anything in buttons.neverPress.
  - Long flows run INSIDE the page: one Runtime.evaluate / javascript call times out at about 45 s on big Play pages (the IARC questionnaire, App content), so never loop over many controls in one call. Paste plan.inPageLoop once per page (one short call; it defines window.__sb), start the job with window.__sb.run([{ role: "radio", label: "No", within: "<question text>" }, { role: "checkbox", label: "<box label>", want: true }, ...]) (it returns at once), then poll window.__log.slice(-20) and window.__done in short calls every 2-5 s until the log says done. The helper only picks radios and ticks boxes (never agreements); press Next / Save yourself afterwards, by their visible text.
  - Wait for each step to render (poll for the next heading or input, up to ~10 s) before acting; never act on a step you have not read.
  - An answer with status ask-owner: ask the owner in chat before filling it (AGENTS.md rule 5), or leave it empty and tell the owner. Never guess.
  - Stop before the final button (buttons.final) and tell the owner exactly what to press; then run the verify command.
  - Never sign in, type a password, accept an agreement or tick a declaration: the owner does that in the browser.

In-page helper (plan.inPageLoop: paste it once per page in one short call, then window.__sb.run([...]) and poll window.__log):
    (() => {
      const norm = (t) => (t || "").replace(/\s+/g, " ").trim();
      const log = (m) => { window.__log.push(new Date().toISOString().slice(11, 19) + " " + m); };
      const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
      const labelOf = (el) => {
        const own = norm(el.getAttribute("aria-label"));
        if (own) return own;
        for (let a = el; a && a !== document.body; a = a.parentElement) { const t = norm(a.innerText); if (t) return t; }
        return "";
      };
      const NEVER = /terms of use|terms of service|i agree|agree to|declaration|developer program polic|export laws/i;
      const CONTROLS = "[role=radio], [role=checkbox], input[type=radio], input[type=checkbox]";
      const scopeOf = (text) => {
        const want = norm(text).toLowerCase(); let best = null;
        for (const el of document.querySelectorAll("section, fieldset, div, [role=radiogroup], [role=group]"))
          if (norm(el.innerText).toLowerCase().includes(want) && el.querySelector(CONTROLS) && (!best || best.contains(el))) best = el;
        return best;
      };
      const find = (role, label, scope) => {
        const want = norm(label).toLowerCase();
        const hits = [...(scope || document).querySelectorAll("[role=" + role + "], input[type=" + role + "]")]
          .filter((el) => el.getClientRects().length && labelOf(el).toLowerCase().startsWith(want));
        return hits.length === 1 ? hits[0] : null;
      };
      const set = async (s) => {
        const scope = s.within ? scopeOf(s.within) : null;
        if (s.within && !scope) return "no block with \"" + s.within + "\"";
        const el = find(s.role, s.label, scope);
        if (!el) return "not found or ambiguous";
        if (NEVER.test(labelOf(el))) return "refused: an agreement (the owner ticks it)";
        const host = el.closest("material-radio, material-checkbox");
        const inner = el.matches("[role=radio], [role=checkbox]") ? el : (host && host.querySelector("[role=radio], [role=checkbox]")) || el;
        const state = () => (inner.hasAttribute("aria-checked") ? inner.getAttribute("aria-checked") === "true" : !!inner.checked);
        const want = s.want !== false;
        if (state() !== want) inner.click();
        await sleep(150);
        return state() === want ? "ok" : "did not change (read the control, try the <input> itself)";
      };
      window.__log = []; window.__done = false;
      window.__sb = { log, sleep, labelOf, scopeOf, find, set };
      window.__sb.run = (steps) => {
        window.__done = false;
        window.__run = (async () => {
          for (let i = 0; i < steps.length; i++) {
            const s = steps[i], tag = (i + 1) + "/" + steps.length + " " + s.role + " \"" + s.label + "\"" + (s.within ? " in \"" + s.within + "\"" : "");
            try { log(tag + ": " + await set(s)); } catch (e) { log(tag + ": error " + e.message); }
          }
          log("done"); window.__done = true;
        })();
        return "started " + steps.length + " steps: poll window.__log";
      };
      return "storebound helper ready: window.__sb.run([...]), then poll window.__log.slice(-20) and window.__done";
    })()

Verify: storebound console verify play-content-rating --from-text -
  - The Summary shows each question with the Yes / No of the answer sheet, and a rating per authority.

Ask the owner first:
  - Category: ask the owner (Pick the category; set console.contentRating.category to have it filled.)
  - IARC Terms of Use: an agreement, the owner ticks it
  - Violence: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.violence.humans, violence.nonHumans, violence.setting, violence.childlike, violence.reactions, violence.presentation, violence.blood, violence.historicalWar, violence.innocents, violence.fierceSounds too.)
  - Fear / horror: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.fear.other too.)
  - Sexuality or nudity: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.sexuality.other too.)
  - Language (profanity): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.language.other too.)
  - Controlled substances (drugs, alcohol, tobacco): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.controlledSubstances.other too.)
  - Crude humour: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.crudeHumor.other too.)
  - Gambling with real money: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.)
  - Simulated gambling: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.)
  - Users can interact or exchange content: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.userInteraction.block, userInteraction.report, userInteraction.moderation, userInteraction.friendsOnly too.)
  - Shares the user's location with other users: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled.)
  - Digital purchases: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.digitalPurchases.playerTrading too.)
  - Unrestricted internet access (browser): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled.)
  - Miscellaneous: Nazi symbols or references?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.naziSymbols to have it filled.)
  - Miscellaneous: content about Korean history that may be sensitive?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.koreaHistory to have it filled.)
  - Miscellaneous: terrorism or extremist content?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.terrorism to have it filled.)
  - Miscellaneous: depicts or encourages crimes?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.crimes to have it filled.)
