Task gender-typing survey

Human survey draft for co-author review

SampleN ≈ 250 on Prolific, balanced by sex (≈125 men / ≈125 women)
DeviceDesktop or laptop only — the grid task needs a real pointer
Length≈ 20 minutes
Pay$5 flat for completing it (≈$15/hr, above Prolific's ≈$12/hr floor), plus a $2 bonus to the top 10% on each timed task — best of three attempts, ties broken by speed where possible, ranked after collection and paid via Prolific. Maximum $4 in bonuses, so up to $9 per participant.
Budget≈$1,250 in base pay + ≈$100 in bonuses + Prolific's fee ≈ $1,800
ApprovalRuns under the existing UT IRB approval, with a rewritten Procedures paragraph. UT IRB only — not UMD.
Blocks:consentanti_aiprolific_idtask_zip_introtask_zip_retrytask_zip_attemptstask_rotationperceptiondemographicsdebrief
Part 1 of 2 — for YongyiSetup: build this firstno participant sees any of this
This survey is delivered as a QSF file — Qualtrics Survey Format, a single file holding an entire survey, exported from one Qualtrics account and imported into another. Importing it rebuilds every block, question and flow element in one step, so nothing has to be assembled by hand. The steps below are the order to work in; the code boxes throughout Part 2 are there to verify the import and to repair anything that did not survive it.
  1. Check two account permissions
    Allow JavaScript and Allow All HTML Markup, under account settings. Do this before anything else. Without them the import fails when the file carries custom code, or — worse — it succeeds and then renders blank for participants while still looking correct in the editor.
  2. Import the QSF file into a throwaway draft survey first
    That import is the real test, and it is the whole build in one step: it creates every block, every question, the Survey Flow, and the embedded-data declarations. If it imports clean and previews correctly, steps 3–6 are already done and you only need step 7. Everything below exists so you can check the import, and rebuild by hand anything that did not survive it.
  3. Survey Flow → declare the embedded data
    One Set Embedded Data element, first in the flow, listing all 74 fields shown further down — every value left blank. Then a second element capturing PROLIFIC_PID from the Prolific URL parameter.
  4. Create the blocks, in this order
    consentanti_aiprolific_idtask_zip_introtask_zip_retrytask_zip_attemptstask_rotationperceptiondemographicsdebrief  — one block per section below. Each block section tells you how many questions it holds, what type each one is, and gives you its HTML and JavaScript to paste.
  5. Paste each question
    For every question: set the question type shown on its label, paste the HTML into the question's HTML View, and where a JavaScript box is shown, paste that into the question's JavaScript editor. Set the export tag to the name on the dark label — the analysis code keys on those names.
  6. Add the branches and the ending
    The consent decline branch, the comprehension retry branch, and the attention-check branch, all shown in the flow table below, then an End of Survey element redirecting to the Prolific completion URL.
  7. Test before launch
    One full Preview click-through. Confirm every embedded-data field actually records a value — a blank column means the field was never declared, which is the most common way a task silently loses all its data. Then a small Prolific soft launch to measure the median completion time, and set the reward to it.
the finished Survey Flowwhat it should look like when you are done
Set Embedded DataAll 74 fields listed below, every value left blank. This must be the first element in the flow.
Set Embedded DataPROLIFIC_PID, STUDY_ID and SESSION_ID captured from the Prolific URL parameters. Prolific recommends all three; SESSION_ID is what disambiguates a participant who re-enters the study.
Blockconsent
BranchIf consent_agree = “I do not agree” → End of Survey, flagged Screened, redirecting to the Prolific screen-out URL.
Blockanti_ai
Blockprolific_id
Set Embedded Datamodule_order = perf_first. To switch to a 50/50 order randomization later, replace this element with a Randomizer over the two module orders and branch on the value.
Blocktask_zip_intro  PERFORMANCE MODULE — instructions and the comprehension check
BranchIf comp_select or comp_bonus is wrong → block task_zip_retry, then Set Embedded Data comp_retry = 1. On the pass path set comp_retry = 0, so blank never means “passed”. This branch has to sit HERE, between the intro and the attempts — a Qualtrics branch cannot run inside a block, so with the check and the attempts together the retry came after all three scored rounds.
Blocktask_zip_attempts  PERFORMANCE MODULE — the three scored attempts
Blocktask_rotation  PERFORMANCE MODULE
Blockperception — the cognitive tasks
Blockperception_anchors — the two sanity-check extremes, on their own page and last
Blockdemographics
BranchIf the instructed attention row = “Disagree” → set attn_pass = 1; else → set attn_pass = 0. Both branches are required: with only the pass branch a genuine failure looks identical to a break-off before the grid. The grid and these branches must sit in separate blocks, and attn_pass must be declared before them.
Blockdebrief
End of SurveyRedirect to the Prolific completion URL.
embedded data74 fieldsdeclare every value BLANK
Declare all of these in that single Set Embedded Data element at the top of the flow, with empty values. Do not give them a default such as -1. The previous build did, and a -1 in the export then means three indistinguishable things: the JS never ran, the write never committed, or the participant never reached that question. A blank makes all three visibly missing. This list is generated from the same source the task JS derives its field names from, so the two cannot disagree.
PROLIFIC_PIDSTUDY_IDSESSION_IDmodule_orderanti_llm_acknowledgedcomp_selectcomp_bonuscomp_select_2comp_bonus_2comp_retryattn_passQ_TerminateFlagzip_a1_scorezip_a1_timezip_a1_foundzip_a2_scorezip_a2_timezip_a2_foundzip_a3_scorezip_a3_timezip_a3_foundrot_a1_scorerot_a1_correctrot_a1_incorrectrot_a1_attemptedrot_a1_timerot_a2_scorerot_a2_correctrot_a2_incorrectrot_a2_attemptedrot_a2_timerot_a3_scorerot_a3_correctrot_a3_incorrectrot_a3_attemptedrot_a3_timeperc_word_search_top10perc_word_search_perfperc_zip_search_top10perc_zip_search_perfperc_number_grid_count_top10perc_number_grid_count_perfperc_mental_rotation_top10perc_mental_rotation_perfperc_spelling_top10perc_spelling_perfperc_proofreading_top10perc_proofreading_perfperc_dsst_top10perc_dsst_perfperc_anagrams_top10perc_anagrams_perfperc_stroop_top10perc_stroop_perfperc_arithmetic_top10perc_arithmetic_perfperc_ravens_top10perc_ravens_perfperc_verbal_fluency_top10perc_verbal_fluency_perfperc_finger_tapping_top10perc_finger_tapping_perfperc_emotion_recognition_top10perc_emotion_recognition_perfperc_reaction_time_top10perc_reaction_time_perfperc_applying_makeup_top10perc_applying_makeup_perfperc_lifting_heavy_top10perc_lifting_heavy_perfperc_task_orderperc_item_orderperc_anchor_orderperc_anchor_item_order
Everything below this line is what a participant sees, in order. Each gray box names the Qualtrics block to create and what it records; the collapsed boxes hold the code to paste.
Block 2anti_ainot shown to participants
Create a Qualtrics block named anti_ai holding 1 question, in the order below.
Keep immediately after consent, before anything else.
Screen 2 of 22

Before you begin

This study measures what people think, and how people perform. For the results to mean anything, we need your own honest impressions and your own unaided effort.

Please do not use ChatGPT or any other AI assistant, and do not ask another person for help, at any point in this survey. There are no right or wrong answers to the opinion questions, so there is nothing to look up. On the two timed tasks, outside help would make the data unusable.

We are not able to detect this perfectly, so we are simply asking you directly.

  • I agree to complete this survey on my own, without AI and without help from others
anti_llm_acknowledgedMultiple Choice (MC / SAVR)Force Response
ANTI-AI PLEDGE — Multiple Choice (MC/SAVR), Force Response. Writes anti_llm_acknowledged. Keep immediately after consent.
Records: anti_llm_acknowledged
HTML to paste into the question's HTML View604 bytes
<p><strong>Before you begin</strong></p>
<p>This study measures what <em>people</em> think, and how <em>people</em> perform. For the results to
mean anything, we need your own honest impressions and your own unaided effort.</p>
<p>Please do not use ChatGPT or any other AI assistant, and do not ask another person for help, at any
point in this survey. There are no right or wrong answers to the opinion questions, so there is nothing
to look up. On the two timed tasks, outside help would make the data unusable.</p>
<p>We are not able to detect this perfectly, so we are simply asking you directly.</p>
Block 3prolific_idnot shown to participants
Create a Qualtrics block named prolific_id holding 1 question, in the order below.
PROLIFIC_PID is captured from the URL by an Embedded Data element at the very top of the Survey Flow. This screen only confirms it arrived, so a missing ID is caught before the participant spends 15 minutes. The piped field shows the participant their own ID.
Screen 3 of 22

Your Prolific ID has been recorded as:

${e://Field/PROLIFIC_PID}5f3a9c2e14b07d8619ae4f22

If that box is empty, please return to Prolific and re-enter the study using the link there, so we can pay you. If it shows your ID, click Next to begin.

pid_confirmDescriptive Text (DB)
PROLIFIC ID CONFIRMATION — Descriptive Text (DB). PROLIFIC_PID arrives as a URL parameter and is captured by an Embedded Data element at the top of the Survey Flow. This screen only confirms it landed, so a missing ID is caught before the participant spends 15 minutes.
Records: PROLIFIC_PID (set from the URL at the top of the flow, not here)
HTML to paste into the question's HTML View311 bytes
<p>Your Prolific ID has been recorded as:</p>
<p style="font-size:20px;font-weight:600;text-align:center;margin:18px 0;">${e://Field/PROLIFIC_PID}</p>
<p>If that box is empty, please return to Prolific and re-enter the study using the link there, so we
can pay you. If it shows your ID, click Next to begin.</p>
Block 4task_zip_introPERFORMANCE MODULEnot shown to participants
Create a Qualtrics block named task_zip_intro holding 3 questions on 2 pages, in the order below.
Instructions and the comprehension check. This is a SEPARATE block from the attempts, and that split is the point: a Qualtrics branch can only sit between blocks, so with the check and the attempts in one block the retry fired after all three scored rounds — retraining the participant only once their paid attempts were already spent. This module also goes BEFORE the perception blocks, because asking who performs better immediately before a paid timed task is a stereotype-threat prime that would bias the very gap we are measuring.
Screen 4 of 22

Task: ZIP-code search

You will see a 7×7 grid of digits and a short list of ZIP codes. Your job is to find each ZIP code hidden in the grid as quickly as you can.

Each ZIP code is five digits in a row, running in a straight line: across, down, or diagonally, in any direction (including backwards). To select one, you can either:

  • Click and drag across its five digits, or
  • Click the first digit, then click the last digit.

Worked example. In the grid below, the ZIP code 90210 runs left to right in the middle row. Selecting it (by dragging across it, or clicking the 9 then the 0) turns it green and crosses it off the list:

6604876
4759382
4219489
2902104
1157815
6593877
8408016

You will do this task three times, each for 30 seconds, with a new grid each time. The same ZIP codes are used each time, so you can get faster with practice. Most people improve across the three rounds, so keep going even if the first round feels hard.

On each task, the top 10% of participants earn a $2 bonus, based on your best of the three rounds. If several people tie on the same score, ties are broken by speed — how quickly you finished that round.

Click Next when you are ready to begin.

zip_instructionsDescriptive Text (DB)
ZIP-code search INSTRUCTIONS (shown once before the first attempt). Static worked example; no timer, no scoring. Paste into a question's HTML View.
HTML to paste into the question's HTML View2.7 KB
<div style="font-family:Arial,Helvetica,sans-serif;max-width:640px;margin:0 auto;font-size:15px;line-height:1.5;color:#222;">
  <h3 style="margin:0 0 10px;">Task: ZIP-code search</h3>
  <p>You will see a <strong>7&times;7 grid of digits</strong> and a short list of ZIP codes. Your job is to find each ZIP code
  hidden in the grid as quickly as you can.</p>
  <p>Each ZIP code is five digits in a row, running in a straight line: <strong>across, down, or
  diagonally, in any direction</strong> (including backwards). To select one, you can either:</p>
  <ul>
    <li><strong>Click and drag</strong> across its five digits, or</li>
    <li><strong>Click the first digit, then click the last digit.</strong></li>
  </ul>

  <p style="margin-top:18px;"><strong>Worked example.</strong> In the grid below, the ZIP code
  <strong>90210</strong> runs left to right in the middle row. Selecting it (by dragging across it,
  or clicking the <em>9</em> then the <em>0</em>) turns it green and crosses it off the list:</p>

  <style>
    /* Scoped to #zip-eg. Bare td/table selectors would restyle every other
       table on the page, including Qualtrics' own matrix questions. */
    #zip-eg{border-collapse:collapse;margin:12px auto;font:20px Arial;font-weight:600;}
    #zip-eg td{border:1px solid #ccc;width:36px;height:36px;text-align:center;}
    #zip-eg td.hl{background:#c9f3cf;color:#147a2b;}
  </style>
  <table id="zip-eg">
      <tr><td>6</td><td>6</td><td>0</td><td>4</td><td>8</td><td>7</td><td>6</td></tr>
      <tr><td>4</td><td>7</td><td>5</td><td>9</td><td>3</td><td>8</td><td>2</td></tr>
      <tr><td>4</td><td>2</td><td>1</td><td>9</td><td>4</td><td>8</td><td>9</td></tr>
      <tr><td>2</td><td class="hl">9</td><td class="hl">0</td><td class="hl">2</td><td class="hl">1</td><td class="hl">0</td><td>4</td></tr>
      <tr><td>1</td><td>1</td><td>5</td><td>7</td><td>8</td><td>1</td><td>5</td></tr>
      <tr><td>6</td><td>5</td><td>9</td><td>3</td><td>8</td><td>7</td><td>7</td></tr>
      <tr><td>8</td><td>4</td><td>0</td><td>8</td><td>0</td><td>1</td><td>6</td></tr>
  </table>

  <p style="margin-top:18px;">You will do this task <strong>three times</strong>, each for <strong>30 seconds</strong>,
  with a new grid each time. The same ZIP codes are used each time, so you can get faster with practice.
  Most people improve across the three rounds, so keep going even if the first round feels hard.</p>
  <p>On each task, the <strong>top 10% of participants earn a $2 bonus</strong>, based on your best of the
  three rounds. If several people tie on the same score, ties are broken by speed &mdash; how quickly you
  finished that round.</p>
  <p>Click <strong>Next</strong> when you are ready to begin.</p>
</div>
Screen 5 of 22
2 questions on this one page — no page break between them.

Before you start: how do you record an answer in this task?

  • Click and drag across the five digits, or click the first digit and then the last
  • Type the code into a box
  • Press the number keys on your keyboard

And what earns the $2 bonus on a task?

  • Scoring in the top 10% of participants, on my best of three rounds
  • Finishing the study
  • Answering every question
comp_selectMultiple Choice (MC / SAVR)Force ResponseRandomize Choices
Forced-correct with ONE retry, built as a duplicated block plus branch logic rather than custom validation, so the two attempts arrive as separate export columns and the retry cannot loop forever. Turn on Randomize Choices: the correct option is first in both items, so without it a participant who clicks the top option twice passes.
Records: comp_select
HTML to paste into the question's HTML View66 bytes
<p>Before you start: how do you record an answer in this task?</p>
comp_bonusMultiple Choice (MC / SAVR)Force ResponseRandomize Choices
Records: comp_bonus
HTML to paste into the question's HTML View45 bytes
<p>And what earns the $2 bonus on a task?</p>
Block 5task_zip_retrySHOWN ONLY ON A WRONG ANSWERnot shown to participants
Create a Qualtrics block named task_zip_retry holding 2 questions on 1 page, in the order below.
Reached only by the branch after task_zip_intro, and only if either comprehension answer was wrong. Same two questions, new export tags, with a short corrective line above them. After this block the flow sets comp_retry = 1 and continues to the attempts either way — this is one retry, not a loop.
Screen 6 of 22
2 questions on this one page — no page break between them.

Not quite — let's check once more before you start.

How do you record an answer in this task?

  • Click and drag across the five digits, or click the first digit and then the last
  • Type the code into a box
  • Press the number keys on your keyboard

And what earns the $2 bonus on a task?

  • Scoring in the top 10% of participants, on my best of three rounds
  • Finishing the study
  • Answering every question
comp_select_2Multiple Choice (MC / SAVR)Force ResponseRandomize Choices
Records: comp_select_2
HTML to paste into the question's HTML View108 bytes
<p>Not quite — let's check once more before you start.</p><p>How do you record an answer in this task?</p>
comp_bonus_2Multiple Choice (MC / SAVR)Force ResponseRandomize Choices
Records: comp_bonus_2
HTML to paste into the question's HTML View45 bytes
<p>And what earns the $2 bonus on a task?</p>
Block 6task_zip_attemptsPERFORMANCE MODULEnot shown to participants
Create a Qualtrics block named task_zip_attempts holding 3 questions (2 of them copies of an attempt question), in the order below.
Three scored attempts. The only line that differs between the three questions is var ATTEMPT = 1; → 2 → 3 ; every field name derives from it. The same five ZIP codes recur across attempts, deliberately, so practice can show. A round ends early once all five are found, so zip_a{n}_time is genuine elapsed time for anyone who finished — which is what breaks bonus ties.
Screens 7–9 of 22 (the same screen 3 times — one per attempt, a new puzzle each time)
The puzzle below is an example. The live task generates a fresh one for every attempt.

Find each ZIP code in the grid. Either click and drag across its five digits, or click the first digit and then the last digit. Codes run in a straight line (across, down, or diagonally) and in any direction.

Found 0 of 5
30s
  • 9 0 2 1 0 (Beverly Hills, CA)
  • 6 0 6 0 1 (Chicago, IL)
  • 1 0 0 0 1 (New York, NY)
  • 2 0 0 0 1 (Washington, DC)
  • 9 4 1 0 2 (San Francisco, CA)
5320919
8961402
0422706
3190004
7059010
8237209
6060131
zip_a1Descriptive Text (DB) + question JS×3 copies
ZIP-code search — Qualtrics Question TEXT (HTML view). One copy per attempt (1,2,3); the only thing that differs across copies is the attempt number in the JS config. The engine builds the grid, target list, timer, and status into #gs-mount below.
Records: zip_a1_score, zip_a1_time, zip_a1_found, … and zip_a2_*, zip_a3_* from copies 2 and 3
HTML to paste into the question's HTML View300 bytes
<p style="font-size:15px;">Find each ZIP code in the grid. Either <strong>click and drag</strong> across its five digits, or
<strong>click the first digit and then the last digit</strong>. Codes run in a straight line (across, down,
or diagonally) and in any direction.</p>

<div id="gs-mount"></div>
JavaScript to paste into the question's JS editor (dist/zip_attempt1.js)22.2 KB
/* AUTO-GENERATED by build_dist.py — do not edit here. Edit the source
 * (files under lib/ and tasks/) and re-run build_dist.py. */

/* ============================================================================
 * qualtrics_api.js — the ONLY place the task engines touch Qualtrics.
 *
 * Why this file exists
 * --------------------
 * The embedded-data setters and the page-submit hook live on the STATIC
 * `Qualtrics.SurveyEngine` namespace, NOT on the question instance that
 * `this` refers to inside addOnload/addOnReady. Calling them on the instance
 * (`q.setJSEmbeddedData(...)`) throws, and every recorded value is lost — the
 * task renders and plays perfectly, then writes nothing. Guarding the call
 * (`if (q.addOnPageSubmit) ...`) makes that failure silent, which is worse.
 *
 * So: engines call QAPI.setED / QAPI.onSubmit. Those resolve the real static
 * API at call time and THROW LOUDLY if it is not there. A missing API must
 * break the local harness and the Qualtrics preview, not quietly drop data.
 *
 * Layout compatibility
 * --------------------
 * `setJSEmbeddedData` is the current setter and the only one that works in the
 * New Survey Taking Experience; `setEmbeddedData` is deprecated but still works
 * in the legacy layout. We prefer the former and fall back to the latter, so
 * the same code is correct under either layout.
 *
 * Values are coerced to String, and null/undefined become "" rather than the
 * literal text "null"/"undefined".
 * ==========================================================================*/
(function (root) {
  "use strict";

  function engine() {
    return root.Qualtrics && root.Qualtrics.SurveyEngine;
  }

  /* Write one embedded-data field. Throws if no setter is available. */
  function setED(name, value) {
    var E = engine();
    if (!E) {
      throw new Error("QAPI.setED(" + name + "): Qualtrics.SurveyEngine is not available");
    }
    var v = (value === null || value === undefined) ? "" : String(value);
    if (typeof E.setJSEmbeddedData === "function") {
      E.setJSEmbeddedData(name, v);
      return;
    }
    if (typeof E.setEmbeddedData === "function") {
      E.setEmbeddedData(name, v);
      return;
    }
    throw new Error("QAPI.setED(" + name + "): no embedded-data setter on " +
                    "Qualtrics.SurveyEngine — the value would have been lost");
  }

  /* Read one embedded-data field back, with a fallback. Never pipe ${e://...}
   * into JS to do this: piped text is resolved server-side before the page is
   * sent, so it is stale, and a quote in the value is a syntax error. */
  function getED(name, fallback) {
    var E = engine();
    var raw = (E && typeof E.getJSEmbeddedData === "function")
      ? E.getJSEmbeddedData(name) : null;
    return (raw === null || raw === undefined || raw === "") ? fallback : raw;
  }

  /* Register a page-submit handler — where all data writes belong. Data written
   * in addOnUnload races the page POST and is routinely lost. */
  function onSubmit(fn) {
    var E = engine();
    if (!E || typeof E.addOnPageSubmit !== "function") {
      throw new Error("QAPI.onSubmit: Qualtrics.SurveyEngine.addOnPageSubmit is " +
                      "unavailable — results would never be written");
    }
    E.addOnPageSubmit(fn);
  }

  root.QAPI = { setED: setED, getED: getED, onSubmit: onSubmit };
})(typeof window !== "undefined" ? window : this);

/* ============================================================================
 * grid_search.js — robust grid drag-select search task (ZIP codes / words).
 * ----------------------------------------------------------------------------
 * One engine drives both the number-string (ZIP) search and the word search.
 * The participant drags across a straight line of cells; on release the line
 * is read in both directions and matched against the remaining targets.
 *
 * Design notes (fixes for the old build's bugs):
 *   - Hit-detection works on explicit cell COORDINATES and the underlying grid
 *     data, never by scanning CSS class strings (old bug: some targets, e.g.
 *     LA, failed to register because className equality was brittle).
 *   - Matches BOTH directions, so dragging end-to-start also counts.
 *   - mouseup is bound to the document, so releasing off-grid still finalizes.
 *   - Found cells carry a separate class and are never disturbed by later drags.
 *   - Results are written with the question instance's setJSEmbeddedData to
 *     explicit, clearly named fields (no read-modify-write append, no -1
 *     default that sticks). Each attempt is its own question -> its own fields.
 *
 * Pure functions (generateGrid / pathCells / readPath / matchTarget) take an
 * injectable RNG and touch no DOM, so they are unit-tested under node. The DOM
 * wiring lives in run().
 * ==========================================================================*/
(function (root, factory) {
  if (typeof module === "object" && module.exports) module.exports = factory();
  else root.GridSearch = factory();
})(typeof self !== "undefined" ? self : this, function () {
  "use strict";

  /* ---------------------------------------------------------------- pure ---- */

  // Deterministic RNG (mulberry32) for reproducible tests / grids.
  function makeRng(seed) {
    var s = seed >>> 0;
    return function () {
      s |= 0; s = (s + 0x6d2b79f5) | 0;
      var t = Math.imul(s ^ (s >>> 15), 1 | s);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }

  var DIRS = [
    [0, 1], [0, -1], [1, 0], [-1, 0],
    [1, 1], [1, -1], [-1, 1], [-1, -1]
  ];

  function shuffle(arr, rng) {
    for (var i = arr.length - 1; i > 0; i--) {
      var j = Math.floor(rng() * (i + 1));
      var t = arr[i]; arr[i] = arr[j]; arr[j] = t;
    }
    return arr;
  }

  // Place every target string into an n x n grid (random cell + direction).
  // Overlap is allowed only where the shared cell holds the same character.
  // Returns {grid: char[][], placements:[{target,r,c,dr,dc}]}.
  function generateGrid(targets, n, fillFn, rng) {
    rng = rng || Math.random;
    for (var attempt = 0; attempt < 300; attempt++) {
      var grid = [];
      for (var r = 0; r < n; r++) grid.push(new Array(n).fill(null));
      var placements = [];
      var allPlaced = true;

      for (var ti = 0; ti < targets.length; ti++) {
        var s = String(targets[ti]);
        var L = s.length;
        var starts = [];
        for (var rr = 0; rr < n; rr++) for (var cc = 0; cc < n; cc++) starts.push([rr, cc]);
        shuffle(starts, rng);

        var placed = false;
        for (var si = 0; si < starts.length && !placed; si++) {
          var r0 = starts[si][0], c0 = starts[si][1];
          var dirs = shuffle(DIRS.slice(), rng);
          for (var di = 0; di < dirs.length && !placed; di++) {
            var dr = dirs[di][0], dc = dirs[di][1];
            var rEnd = r0 + dr * (L - 1), cEnd = c0 + dc * (L - 1);
            if (rEnd < 0 || rEnd >= n || cEnd < 0 || cEnd >= n) continue;
            var fits = true;
            for (var k = 0; k < L; k++) {
              var g = grid[r0 + dr * k][c0 + dc * k];
              if (g !== null && g !== s.charAt(k)) { fits = false; break; }
            }
            if (!fits) continue;
            for (var k2 = 0; k2 < L; k2++) grid[r0 + dr * k2][c0 + dc * k2] = s.charAt(k2);
            placements.push({ target: s, r: r0, c: c0, dr: dr, dc: dc });
            placed = true;
          }
        }
        if (!placed) { allPlaced = false; break; }
      }
      if (!allPlaced) continue;

      for (var r2 = 0; r2 < n; r2++) for (var c2 = 0; c2 < n; c2++) {
        if (grid[r2][c2] === null) grid[r2][c2] = fillFn(rng);
      }
      return { grid: grid, placements: placements };
    }
    throw new Error("generateGrid: could not place all targets in " + n + "x" + n);
  }

  // Cells on the straight line start->end (inclusive), or null if not a
  // horizontal / vertical / 45-degree-diagonal line.
  function pathCells(start, end) {
    var dr = end.r - start.r, dc = end.c - start.c;
    var adr = Math.abs(dr), adc = Math.abs(dc);
    if (!(dr === 0 || dc === 0 || adr === adc)) return null;
    var len = Math.max(adr, adc);
    var sr = dr === 0 ? 0 : dr / adr;
    var sc = dc === 0 ? 0 : dc / adc;
    var out = [];
    for (var k = 0; k <= len; k++) out.push({ r: start.r + sr * k, c: start.c + sc * k });
    return out;
  }

  function readPath(grid, cells) {
    var s = "";
    for (var i = 0; i < cells.length; i++) s += grid[cells[i].r][cells[i].c];
    return s;
  }

  // Match str (in either direction) against remaining targets; return it or null.
  function matchTarget(str, remaining) {
    var rev = str.split("").reverse().join("");
    for (var i = 0; i < remaining.length; i++) {
      if (remaining[i] === str || remaining[i] === rev) return remaining[i];
    }
    return null;
  }

  function digitFill(rng) { return String(Math.floor(rng() * 10)); }
  var LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  function letterFill(rng) { return LETTERS.charAt(Math.floor(rng() * 26)); }

  /* ----------------------------------------------------------------- CSS ---- */

  var CSS_ID = "gs-style";
  var CSS = [
    ".gs-wrap{font-family:Arial,Helvetica,sans-serif;max-width:640px;margin:0 auto;}",
    ".gs-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:8px;}",
    ".gs-timer{font-size:20px;font-weight:700;color:#b00;min-width:90px;}",
    ".gs-timer.low{color:#d00;animation:gsblink 1s steps(2,start) infinite;}",
    "@keyframes gsblink{50%{opacity:.35;}}",
    ".gs-progress{font-size:15px;color:#333;}",
    ".gs-targets{list-style:none;padding:0;margin:0 0 12px 0;display:flex;flex-wrap:wrap;gap:6px 16px;font-size:15px;}",
    ".gs-targets li{font-weight:600;letter-spacing:1px;}",
    ".gs-targets li .lbl{font-weight:400;color:#666;letter-spacing:0;}",
    ".gs-targets li.done{color:#999;text-decoration:line-through;}",
    "table.gs-grid{border-collapse:collapse;margin:0 auto;user-select:none;-webkit-user-select:none;-ms-user-select:none;touch-action:none;}",
    "table.gs-grid td{width:38px;height:38px;text-align:center;vertical-align:middle;border:1px solid #ccc;",
    "font-size:20px;font-weight:600;cursor:pointer;color:#222;background:#fff;}",
    "table.gs-grid td.gs-selecting{background:#cfe3ff;}",
    "table.gs-grid td.gs-found{background:#c9f3cf;color:#147a2b;}",
    ".gs-msg{margin-top:12px;min-height:22px;font-size:15px;color:#444;text-align:center;}",
    ".gs-cur{margin-top:6px;height:20px;font:15px monospace;color:#555;text-align:center;letter-spacing:2px;}"
  ].join("\n");

  function ensureCss() {
    if (document.getElementById(CSS_ID)) return;
    var st = document.createElement("style");
    st.id = CSS_ID; st.type = "text/css";
    st.appendChild(document.createTextNode(CSS));
    document.head.appendChild(st);
  }

  /* ------------------------------------------------------------- DOM run ---- */

  // q        : the Qualtrics question instance (`this` inside addOnload)
  // container: the question container element
  // config   : {
  //   taskId:"zip", attempt:1, fill:"digits"|"letters", gridSize:7,
  //   timeLimitSec:45,
  //   targets:[{code:"10001", label:"New York, NY"}, ...],
  //   fields:{score:"zip_a1_score", time:"zip_a1_time", found:"zip_a1_found"},
  //   showLabels:true
  // }
  function run(q, container, config) {
    ensureCss();
    var now = (typeof performance !== "undefined" && performance.now)
      ? function () { return performance.now(); } : function () { return Date.now(); };

    var n = config.gridSize;
    var fillFn = config.fill === "letters" ? letterFill : digitFill;
    var codes = config.targets.map(function (t) { return String(t.code); });
    var gen = generateGrid(codes, n, fillFn, Math.random);
    var grid = gen.grid;

    var remaining = codes.slice();
    var foundByCode = {};
    var foundCells = {};            // "r,c" -> true
    var score = 0;
    var finished = false;
    var startT = now();
    var deadline = startT + config.timeLimitSec * 1000;
    var timerId = null;
    var resultsWritten = false;

    /* ---- build DOM ---- */
    // Mount into #gs-mount if the question provides one (keeps any heading
    // above it); otherwise take over the whole container.
    var mount = (container.querySelector && container.querySelector("#gs-mount")) || container;
    mount.innerHTML = "";
    var wrap = el("div", "gs-wrap");

    var top = el("div", "gs-top");
    var prog = el("div", "gs-progress");
    prog.innerHTML = "Found <strong><span id='gs-score'>0</span></strong> of " + codes.length;
    var timer = el("div", "gs-timer");
    timer.id = "gs-timer";
    top.appendChild(prog); top.appendChild(timer);
    wrap.appendChild(top);

    var list = el("ul", "gs-targets");
    var liByCode = {};
    codes.forEach(function (code, i) {
      var li = el("li");
      var lbl = config.showLabels && config.targets[i].label
        ? " <span class='lbl'>(" + config.targets[i].label + ")</span>" : "";
      li.innerHTML = spaced(code) + lbl;
      list.appendChild(li);
      liByCode[code] = li;
    });
    wrap.appendChild(list);

    var tbl = document.createElement("table");
    tbl.className = "gs-grid";
    for (var r = 0; r < n; r++) {
      var tr = tbl.insertRow(-1);
      for (var c = 0; c < n; c++) {
        var td = tr.insertCell(-1);
        td.textContent = grid[r][c];
        td.setAttribute("data-r", r);
        td.setAttribute("data-c", c);
      }
    }
    wrap.appendChild(tbl);

    var cur = el("div", "gs-cur"); cur.id = "gs-cur";
    wrap.appendChild(cur);
    var msg = el("div", "gs-msg"); msg.id = "gs-msg";
    wrap.appendChild(msg);

    mount.appendChild(wrap);

    /* ---- selection ----
       Supports BOTH click-and-drag AND click-first-cell / click-last-cell
       (two-click). The two-click mode is touchpad- and touch-friendly; the drag
       mode is natural with a mouse. Hit-detection uses explicit cell
       coordinates, never CSS-class scanning. */
    var anchor = null;     // first cell of the in-progress selection
    var armed = false;     // anchor was set by a click; awaiting the second click
    var mouseDown = false;
    var didDrag = false;
    var lastCell = null;   // most recent cell under the pointer
    var curPath = [];

    function cellEl(r, c) { return tbl.rows[r].cells[c]; }
    function key(p) { return p.r + "," + p.c; }
    function same(a, b) { return a && b && a.r === b.r && a.c === b.c; }

    function clearSelecting() {
      for (var i = 0; i < curPath.length; i++) {
        var p = curPath[i];
        if (!foundCells[key(p)]) cellEl(p.r, p.c).classList.remove("gs-selecting");
      }
      curPath = [];
    }
    function renderPath(cells) {
      clearSelecting();
      if (!cells || !cells.length) { cur.textContent = ""; return; }
      for (var i = 0; i < cells.length; i++) {
        var p = cells[i];
        if (!foundCells[key(p)]) cellEl(p.r, p.c).classList.add("gs-selecting");
      }
      curPath = cells;
      cur.textContent = spaced(readPath(grid, cells));
    }

    function cellFromEvent(ev) {
      var t = ev.target;
      var td = t && t.closest ? t.closest("td") : t;
      if (!td || !tbl.contains(td)) return null;   // tbody-safe membership test
      return { r: +td.getAttribute("data-r"), c: +td.getAttribute("data-c") };
    }
    function reset() { anchor = null; armed = false; didDrag = false; clearSelecting(); cur.textContent = ""; }
    function commit(endCell) {
      var p = pathCells(anchor, endCell);
      if (p) { renderPath(p); finalizeSelection(); }
      reset();
    }

    function onDown(ev) {
      if (finished) return;
      var cell = cellFromEvent(ev); if (!cell) return;
      if (ev.preventDefault) ev.preventDefault();
      mouseDown = true;
      if (!anchor) { anchor = cell; lastCell = cell; didDrag = false; renderPath([cell]); }
      else { lastCell = cell; didDrag = false; }   // press for the second click
    }
    function onMove(ev) {
      if (finished || !anchor) return;
      var cell = cellFromEvent(ev); if (!cell) return;
      lastCell = cell;
      if (mouseDown) {                       // dragging
        if (!same(cell, anchor)) didDrag = true;
        var p = pathCells(anchor, cell); if (p) renderPath(p);
      } else if (armed) {                    // two-click: preview line to hovered cell
        var p2 = pathCells(anchor, cell); if (p2) renderPath(p2);
      }
    }
    function onUp(ev) {
      if (finished || !mouseDown) return;
      mouseDown = false;
      if (!anchor) return;
      var cell = (ev && cellFromEvent(ev)) || lastCell;
      if (didDrag) { commit(cell || lastCell); }   // drag selection
      else if (!armed) { armed = true; }           // first click: arm, keep anchor lit
      else if (same(cell, anchor)) { reset(); }    // click anchor again: cancel
      else { commit(cell); }                       // second click elsewhere: select line
    }

    function finalizeSelection() {
      if (curPath.length >= 2) {
        var str = readPath(grid, curPath);
        var hit = matchTarget(str, remaining);
        if (hit) {
          foundByCode[hit] = true;
          remaining.splice(remaining.indexOf(hit), 1);
          score++;
          for (var i = 0; i < curPath.length; i++) {
            var p = curPath[i];
            foundCells[key(p)] = true;
            var c = cellEl(p.r, p.c);
            c.classList.remove("gs-selecting");
            c.classList.add("gs-found");
          }
          if (liByCode[hit]) liByCode[hit].classList.add("done");
          document.getElementById("gs-score").textContent = score;
          flash(msg, "Found " + spaced(hit) + "  (" + score + " of " + codes.length + ")");
          if (score >= codes.length) return finish(true);
        }
      }
      clearSelecting();
      cur.textContent = "";
    }

    // Mouse: down on grid, move over cells (for drag + two-click preview), up on document.
    tbl.addEventListener("mousedown", onDown);
    tbl.addEventListener("mouseover", onMove);
    document.addEventListener("mouseup", onUp);
    // Touch / pen via pointer events (tap-first / tap-last, or drag).
    tbl.addEventListener("pointerdown", function (e) { if (e.pointerType !== "mouse") onDown(e); });
    document.addEventListener("pointermove", function (e) {
      if (e.pointerType === "mouse" || !mouseDown) return;
      var t = document.elementFromPoint(e.clientX, e.clientY);
      onMove({ target: t });
    });
    document.addEventListener("pointerup", function (e) { if (e.pointerType !== "mouse") onUp(e); });

    /* ---- timer ---- */
    function fmt(ms) { return Math.max(0, Math.ceil(ms / 1000)); }
    function tick() {
      var rem = deadline - now();
      var secs = fmt(rem);
      timer.textContent = secs + "s";
      if (secs <= 5) timer.classList.add("low"); else timer.classList.remove("low");
      if (rem <= 0) finish(false);
    }
    tick();
    timerId = setInterval(tick, 200);

    /* ---- finish + record ---- */
    function writeResults() {
      if (resultsWritten) return;
      resultsWritten = true;
      var elapsed = Math.min(config.timeLimitSec, (now() - startT) / 1000);
      QAPI.setED(config.fields.score, score);
      QAPI.setED(config.fields.time, elapsed.toFixed(2));
      if (config.fields.found) {
        QAPI.setED(config.fields.found, Object.keys(foundByCode).join("|"));
      }
    }

    function finish(allFound) {
      if (finished) return;
      finished = true;
      if (timerId) { clearInterval(timerId); timerId = null; }
      timer.textContent = "0s"; timer.classList.remove("low");
      clearSelecting();
      writeResults();
      msg.innerHTML = allFound
        ? "<strong>You found all " + codes.length + "!</strong> Click <em>Next</em> to continue."
        : "Time is up. You found <strong>" + score + " of " + codes.length +
          "</strong>. Click <em>Next</em> to continue.";
      showNext();
    }

    // Record again at page submit as a backstop (handoff: write at submit, not load).
    QAPI.onSubmit(writeResults);

    /* ---- Next/Prev button handling ---- */
    function hideNav() {
      if (q.hideNextButton) q.hideNextButton();
      if (q.hidePreviousButton) q.hidePreviousButton();
      if (window.jQuery) { jQuery("#NextButton").hide(); jQuery("#PreviousButton").hide(); }
    }
    function showNext() {
      if (q.showNextButton) q.showNextButton();
      if (window.jQuery) jQuery("#NextButton").show();
    }
    hideNav();
  }

  /* ----------------------------------------------------------- helpers ---- */
  function el(tag, cls) {
    var e = document.createElement(tag);
    if (cls) e.className = cls;
    return e;
  }
  function spaced(s) { return String(s).split("").join(" "); } // thin-space between chars
  function flash(node, text) { node.textContent = text; }

  return {
    makeRng: makeRng,
    generateGrid: generateGrid,
    pathCells: pathCells,
    readPath: readPath,
    matchTarget: matchTarget,
    digitFill: digitFill,
    letterFill: letterFill,
    run: run
  };
});

/* ZIP-code search — Qualtrics Question JAVASCRIPT.
 * SOURCE form (depends on GridSearch from lib/grid_search.js). The dist build
 * inlines the engine so the pasted question is self-contained.
 *
 * Make THREE copies of this question (attempts 1, 2, 3). The ONLY line that
 * differs across the three copies is `var ATTEMPT = ...;`. Everything else —
 * including the recorded field names — is derived from it, so there is no
 * read-modify-write of embedded data and nothing can stick at a stale value. */
(function () {
  var ATTEMPT = 1; // <-- set to 1, 2, or 3 per copy. Nothing else changes.

  // Five fixed cities, reused every attempt (learning across attempts is wanted).
  // New York (10001, many 0s/1s) is placed 3rd so it is not the first sticking
  // point that kills persistence.
  var TARGETS = [
    { code: "90210", label: "Beverly Hills, CA" },
    { code: "60601", label: "Chicago, IL" },
    { code: "10001", label: "New York, NY" },
    { code: "20001", label: "Washington, DC" },
    { code: "94102", label: "San Francisco, CA" }
  ];

  Qualtrics.SurveyEngine.addOnload(function () {
    GridSearch.run(this, this.getQuestionContainer(), {
      taskId: "zip",
      attempt: ATTEMPT,
      fill: "digits",
      gridSize: 7,
      timeLimitSec: 30,
      showLabels: true,
      targets: TARGETS,
      fields: {
        score: "zip_a" + ATTEMPT + "_score",
        time: "zip_a" + ATTEMPT + "_time",
        found: "zip_a" + ATTEMPT + "_found"
      }
    });
  });
})();
Make 3 copies of this question. The only difference between them is the line var ATTEMPT = 1;23. Every field name derives from it, so nothing else changes.
Block 7task_rotationPERFORMANCE MODULEnot shown to participants
Create a Qualtrics block named task_rotation holding 4 questions (2 of them copies of an attempt question), in the order below.
Second timed task, three attempts. Scored as net correct (correct − incorrect) so guessing is not rewarded. For the reported ability measure use the AVERAGE of the three attempts, not the max — the max is upward-biased for high-variance responders. Best-of-three is for the bonus only.
Screen 10 of 22

Task: Mental rotation

You will see two shapes side by side. Decide whether the shape on the right is the same shape as the one on the left, just turned (rotated), or whether it is the shape's mirror image. Click Same or Mirror image; a new pair appears right away.

Example 1 — Same shape, rotated. The right shape is the left shape turned a quarter-turn. The correct answer is Same:

vs

Example 2 — Mirror image. The right shape cannot be made by turning the left shape; it is flipped. The correct answer is Mirror image:

vs

Each correct answer earns a point and each wrong answer loses a point, so answering at random will not help: answer carefully rather than guessing.

You will do this task three times, each for 30 seconds. On each task, the top 10% of participants earn a $2 bonus, based on your best of the three rounds. Every round here runs for the full time, so if several people tie on the same score, everyone tied receives the bonus.

Click Next when you are ready to begin.

rot_instructionsDescriptive Text (DB)
Mental rotation INSTRUCTIONS (shown once before the first attempt). Static worked examples.
HTML to paste into the question's HTML View3.6 KB
<div style="font-family:Arial,Helvetica,sans-serif;max-width:640px;margin:0 auto;font-size:15px;line-height:1.5;color:#222;">
  <h3 style="margin:0 0 10px;">Task: Mental rotation</h3>
  <p>You will see two shapes side by side. Decide whether the shape on the right is the
  <strong>same</strong> shape as the one on the left, just turned (rotated), or whether it is the
  shape's <strong>mirror image</strong>. Click <strong>Same</strong> or <strong>Mirror image</strong>;
  a new pair appears right away.</p>

  <p style="margin-top:16px;"><strong>Example 1 — Same shape, rotated.</strong> The right shape is the
  left shape turned a quarter-turn. The correct answer is <strong>Same</strong>:</p>
  <div style="display:flex;gap:24px;align-items:center;justify-content:center;margin:8px 0;">
    <!-- reference: X. / X. / XX -->
    <svg width="56" height="80" viewBox="-4 -4 56 80"><rect x="0" y="0" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="0" y="24" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="0" y="48" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="24" y="48" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/></svg>
    <span style="color:#888;">vs</span>
    <!-- same, rotated 90deg: XXX / X.. -->
    <svg width="80" height="56" viewBox="-4 -4 80 56"><rect x="0" y="0" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="24" y="0" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="48" y="0" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="0" y="24" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/></svg>
  </div>

  <p style="margin-top:16px;"><strong>Example 2 — Mirror image.</strong> The right shape cannot be made
  by turning the left shape; it is flipped. The correct answer is <strong>Mirror image</strong>:</p>
  <div style="display:flex;gap:24px;align-items:center;justify-content:center;margin:8px 0;">
    <!-- reference: X. / X. / XX -->
    <svg width="56" height="80" viewBox="-4 -4 56 80"><rect x="0" y="0" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="0" y="24" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="0" y="48" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="24" y="48" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/></svg>
    <span style="color:#888;">vs</span>
    <!-- mirror: .X / .X / XX -->
    <svg width="56" height="80" viewBox="-4 -4 56 80"><rect x="24" y="0" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="24" y="24" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="0" y="48" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/><rect x="24" y="48" width="24" height="24" fill="#3b6fb0" stroke="#1c3c63" stroke-width="2"/></svg>
  </div>

  <p style="margin-top:18px;"><strong>Each correct answer earns a point and each wrong answer loses a
  point</strong>, so answering at random will not help: answer carefully rather than guessing.</p>
  <p>You will do this task <strong>three times</strong>, each for <strong>30 seconds</strong>. On each task, the
  <strong>top 10% of participants earn a $2 bonus</strong>, based on your best of the three rounds.
  Every round here runs for the full time, so if several people tie on the same score, everyone tied
  receives the bonus.</p>
  <p>Click <strong>Next</strong> when you are ready to begin.</p>
</div>
Screens 11–13 of 22 (the same screen 3 times — one per attempt, a new puzzle each time)
The puzzle below is an example. The live task generates a fresh one for every attempt.

Two shapes appear side by side. Decide whether the right shape is the same shape rotated, or its mirror image. Click your answer; a new pair appears right away.

Each correct answer is worth a point and each wrong answer subtracts a point, so answering at random will not help your score: answer carefully rather than guessing.

Item 1
30s
vs
same shape, or mirror image?
Same shape, rotatedMirror image
rot_a1Descriptive Text (DB) + question JS×3 copies
Mental rotation — Qualtrics Question TEXT (HTML view). One copy per attempt.
Records: rot_a1_score (net), rot_a1_correct, rot_a1_incorrect, rot_a1_attempted, rot_a1_time, … and rot_a2_*, rot_a3_* from copies 2 and 3
HTML to paste into the question's HTML View467 bytes
<p style="font-size:15px;">Two shapes appear side by side. Decide whether the right shape is the
<strong>same</strong> shape rotated, or its <strong>mirror image</strong>. Click your answer; a new pair
appears right away.</p>
<p style="font-size:15px;">Each correct answer is worth a point and <strong>each wrong answer subtracts a point</strong>, so
answering at random will not help your score: answer carefully rather than guessing.</p>

<div id="rot-mount"></div>
JavaScript to paste into the question's JS editor (dist/rotation_attempt1.js)15.6 KB
/* AUTO-GENERATED by build_dist.py — do not edit here. Edit the source
 * (files under lib/ and tasks/) and re-run build_dist.py. */

/* ============================================================================
 * qualtrics_api.js — the ONLY place the task engines touch Qualtrics.
 *
 * Why this file exists
 * --------------------
 * The embedded-data setters and the page-submit hook live on the STATIC
 * `Qualtrics.SurveyEngine` namespace, NOT on the question instance that
 * `this` refers to inside addOnload/addOnReady. Calling them on the instance
 * (`q.setJSEmbeddedData(...)`) throws, and every recorded value is lost — the
 * task renders and plays perfectly, then writes nothing. Guarding the call
 * (`if (q.addOnPageSubmit) ...`) makes that failure silent, which is worse.
 *
 * So: engines call QAPI.setED / QAPI.onSubmit. Those resolve the real static
 * API at call time and THROW LOUDLY if it is not there. A missing API must
 * break the local harness and the Qualtrics preview, not quietly drop data.
 *
 * Layout compatibility
 * --------------------
 * `setJSEmbeddedData` is the current setter and the only one that works in the
 * New Survey Taking Experience; `setEmbeddedData` is deprecated but still works
 * in the legacy layout. We prefer the former and fall back to the latter, so
 * the same code is correct under either layout.
 *
 * Values are coerced to String, and null/undefined become "" rather than the
 * literal text "null"/"undefined".
 * ==========================================================================*/
(function (root) {
  "use strict";

  function engine() {
    return root.Qualtrics && root.Qualtrics.SurveyEngine;
  }

  /* Write one embedded-data field. Throws if no setter is available. */
  function setED(name, value) {
    var E = engine();
    if (!E) {
      throw new Error("QAPI.setED(" + name + "): Qualtrics.SurveyEngine is not available");
    }
    var v = (value === null || value === undefined) ? "" : String(value);
    if (typeof E.setJSEmbeddedData === "function") {
      E.setJSEmbeddedData(name, v);
      return;
    }
    if (typeof E.setEmbeddedData === "function") {
      E.setEmbeddedData(name, v);
      return;
    }
    throw new Error("QAPI.setED(" + name + "): no embedded-data setter on " +
                    "Qualtrics.SurveyEngine — the value would have been lost");
  }

  /* Read one embedded-data field back, with a fallback. Never pipe ${e://...}
   * into JS to do this: piped text is resolved server-side before the page is
   * sent, so it is stale, and a quote in the value is a syntax error. */
  function getED(name, fallback) {
    var E = engine();
    var raw = (E && typeof E.getJSEmbeddedData === "function")
      ? E.getJSEmbeddedData(name) : null;
    return (raw === null || raw === undefined || raw === "") ? fallback : raw;
  }

  /* Register a page-submit handler — where all data writes belong. Data written
   * in addOnUnload races the page POST and is routinely lost. */
  function onSubmit(fn) {
    var E = engine();
    if (!E || typeof E.addOnPageSubmit !== "function") {
      throw new Error("QAPI.onSubmit: Qualtrics.SurveyEngine.addOnPageSubmit is " +
                      "unavailable — results would never be written");
    }
    E.addOnPageSubmit(fn);
  }

  root.QAPI = { setED: setED, getED: getED, onSubmit: onSubmit };
})(typeof window !== "undefined" ? window : this);

/* ============================================================================
 * rotation.js — mental-rotation task (same-vs-mirror judgement).
 * ----------------------------------------------------------------------------
 * Each trial shows two shapes side by side. The right shape is the left shape
 * either ROTATED (a "same" trial) or MIRRORED and rotated (a "mirror" trial).
 * The participant clicks "Same" or "Mirror." As many as possible in a fixed
 * time; score = number correct (we also record attempts).
 *
 * Stimuli are random polyominoes generated on the fly (license-clean, no
 * external assets, fully self-contained). Reference shapes are constrained to
 * be CHIRAL — i.e. their mirror image is not equal to any of their own
 * rotations — so "same" vs "mirror" is always unambiguous. "Same" trials use a
 * non-zero rotation, so a genuine mental rotation is required.
 *
 * Note: this is a 2-D polyomino implementation. If a stronger, more classic
 * 3-D Shepard-Metzler male-advantage stimulus is preferred, that is a separate
 * stimulus generator; the task scaffolding here (timer, scoring, fields) is
 * reusable either way.
 *
 * Pure logic (normalize / rotate / mirror / chirality / makeTrial) takes an
 * injectable RNG and touches no DOM; DOM wiring is in run().
 * ==========================================================================*/
(function (root, factory) {
  if (typeof module === "object" && module.exports) module.exports = factory();
  else root.Rotation = factory();
})(typeof self !== "undefined" ? self : this, function () {
  "use strict";

  /* ---------------------------------------------------------------- pure ---- */
  function normalize(cells) {
    var minR = Infinity, minC = Infinity;
    cells.forEach(function (p) { if (p[0] < minR) minR = p[0]; if (p[1] < minC) minC = p[1]; });
    return cells.map(function (p) { return [p[0] - minR, p[1] - minC]; })
      .sort(function (a, b) { return a[0] - b[0] || a[1] - b[1]; });
  }
  function key(cells) { return normalize(cells).map(function (p) { return p[0] + ":" + p[1]; }).join(","); }
  // 90-degree rotation: (r,c) -> (c, -r), then renormalize.
  function rotate(cells) { return normalize(cells.map(function (p) { return [p[1], -p[0]]; })); }
  // reflection across a vertical axis: (r,c) -> (r, -c).
  function mirror(cells) { return normalize(cells.map(function (p) { return [p[0], -p[1]]; })); }

  function rotations(cells) {
    var out = [cells], cur = cells;
    for (var i = 0; i < 3; i++) { cur = rotate(cur); out.push(cur); }
    return out;
  }
  // chiral = mirror image is not congruent to any rotation of the shape.
  function isChiral(cells) {
    var rotKeys = {};
    rotations(cells).forEach(function (r) { rotKeys[key(r)] = true; });
    return !rotKeys[key(mirror(cells))];
  }

  // Random polyomino of `size` connected cells via random growth.
  function genPolyomino(size, rng) {
    rng = rng || Math.random;
    var occ = { "0:0": true }, cells = [[0, 0]];
    var DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    var guard = 0;
    while (cells.length < size && guard++ < 5000) {
      var base = cells[Math.floor(rng() * cells.length)];
      var d = DIRS[Math.floor(rng() * 4)];
      var nr = base[0] + d[0], nc = base[1] + d[1], k = nr + ":" + nc;
      if (!occ[k]) { occ[k] = true; cells.push([nr, nc]); }
    }
    return normalize(cells);
  }
  function genChiral(size, rng) {
    for (var i = 0; i < 200; i++) {
      var p = genPolyomino(size, rng);
      if (isChiral(p)) return p;
    }
    return genPolyomino(size, rng); // fallback (rare)
  }

  // makeTrial -> { reference, comparison, answer:"same"|"mirror" }
  function makeTrial(size, rng, forceSame) {
    rng = rng || Math.random;
    var ref = genChiral(size, rng);
    var same = (typeof forceSame === "boolean") ? forceSame : rng() < 0.5;
    var base = same ? ref : mirror(ref);
    // Rotate by k*90. BOTH trial types require a non-zero rotation. When "mirror"
    // trials were allowed k = 0, a quarter of them showed the comparison as a pure
    // upright reflection, so "looks flipped but still upright => Mirror" was a
    // shortcut that never misfired: free points with no mental rotation, and
    // unequal difficulty between the two response categories.
    var k = 1 + Math.floor(rng() * 3);
    var comp = base;
    for (var i = 0; i < k; i++) comp = rotate(comp);
    return { reference: ref, comparison: comp, answer: same ? "same" : "mirror" };
  }

  /* ----------------------------------------------------------------- CSS ---- */
  var CSS_ID = "rot-style";
  var CSS = [
    ".rot-wrap{font-family:Arial,Helvetica,sans-serif;max-width:600px;margin:0 auto;}",
    ".rot-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;}",
    ".rot-timer{font-size:20px;font-weight:700;color:#b00;min-width:90px;text-align:right;}",
    ".rot-timer.low{color:#d00;animation:rotblink 1s steps(2,start) infinite;}",
    "@keyframes rotblink{50%{opacity:.35;}}",
    ".rot-progress{font-size:15px;color:#333;}",
    ".rot-stims{display:flex;gap:28px;justify-content:center;align-items:center;margin:10px 0 18px;}",
    ".rot-stims svg{background:#fff;border:1px solid #e2e2e2;border-radius:6px;}",
    ".rot-vs{font-size:15px;color:#888;}",
    ".rot-btns{display:flex;gap:16px;justify-content:center;}",
    ".rot-btns button{font-size:17px;padding:10px 26px;border-radius:6px;border:2px solid #1f6feb;",
    "background:#fff;color:#1f6feb;cursor:pointer;min-width:120px;}",
    ".rot-btns button:hover{background:#eef4ff;}",
    ".rot-msg{margin-top:14px;min-height:22px;font-size:15px;color:#444;text-align:center;}"
  ].join("\n");
  function ensureCss() {
    if (document.getElementById(CSS_ID)) return;
    var st = document.createElement("style");
    st.id = CSS_ID; st.type = "text/css";
    st.appendChild(document.createTextNode(CSS));
    document.head.appendChild(st);
  }

  function svgFor(cells, px) {
    var maxR = 0, maxC = 0;
    cells.forEach(function (p) { if (p[0] > maxR) maxR = p[0]; if (p[1] > maxC) maxC = p[1]; });
    var W = (maxC + 1) * px, H = (maxR + 1) * px, pad = 8;
    var rects = cells.map(function (p) {
      return "<rect x='" + (p[1] * px) + "' y='" + (p[0] * px) + "' width='" + px + "' height='" + px +
        "' fill='#3b6fb0' stroke='#1c3c63' stroke-width='2'/>";
    }).join("");
    return "<svg width='" + (W + 2 * pad) + "' height='" + (H + 2 * pad) +
      "' viewBox='" + (-pad) + " " + (-pad) + " " + (W + 2 * pad) + " " + (H + 2 * pad) + "'>" + rects + "</svg>";
  }

  /* ------------------------------------------------------------- DOM run ---- */
  // config: { taskId:"rotation", attempt:1, timeLimitSec:45, shapeSize:6, cellPx:30,
  //           fields:{ score:"rot_a1_score", attempted:"rot_a1_attempted", time:"rot_a1_time" } }
  function run(q, container, config) {
    ensureCss();
    var now = (typeof performance !== "undefined" && performance.now)
      ? function () { return performance.now(); } : function () { return Date.now(); };
    var rng = Math.random;
    var size = config.shapeSize || 6, px = config.cellPx || 30;

    var correct = 0, incorrect = 0, attempted = 0, finished = false, resultsWritten = false;
    var startT = now(), deadline = startT + config.timeLimitSec * 1000, timerId = null;
    var trial = null;

    var mount = (container.querySelector && container.querySelector("#rot-mount")) || container;
    mount.innerHTML = "";
    var wrap = el("div", "rot-wrap");

    var top = el("div", "rot-top");
    var prog = el("div", "rot-progress");
    prog.innerHTML = "Completed: <strong><span id='rot-answered'>0</span></strong>";
    var timer = el("div", "rot-timer"); timer.id = "rot-timer";
    top.appendChild(prog); top.appendChild(timer);
    wrap.appendChild(top);

    var stims = el("div", "rot-stims"); stims.id = "rot-stims"; wrap.appendChild(stims);

    var btns = el("div", "rot-btns");
    var bSame = document.createElement("button"); bSame.type = "button"; bSame.textContent = "Same";
    var bMir = document.createElement("button"); bMir.type = "button"; bMir.textContent = "Mirror image";
    btns.appendChild(bSame); btns.appendChild(bMir);
    wrap.appendChild(btns);

    var msg = el("div", "rot-msg"); msg.id = "rot-msg"; wrap.appendChild(msg);
    mount.appendChild(wrap);

    function newTrial() {
      trial = makeTrial(size, rng);
      stims.innerHTML = svgFor(trial.reference, px) +
        "<span class='rot-vs'>vs</span>" + svgFor(trial.comparison, px);
    }
    // No per-trial feedback and no error-contingent delay: guessing is handled
    // purely by net scoring (correct - incorrect) at the end. This keeps the
    // task a clean ability snapshot with one well-understood scoring rule.
    function answer(ans) {
      if (finished) return;
      attempted++;
      if (ans === trial.answer) correct++; else incorrect++;
      document.getElementById("rot-answered").textContent = attempted;
      newTrial();
    }
    bSame.addEventListener("click", function () { answer("same"); });
    bMir.addEventListener("click", function () { answer("mirror"); });

    function tick() {
      var rem = deadline - now();
      var secs = Math.max(0, Math.ceil(rem / 1000));
      timer.textContent = secs + "s";
      if (secs <= 5) timer.classList.add("low"); else timer.classList.remove("low");
      if (rem <= 0) finish();
    }
    function writeResults() {
      if (resultsWritten) return;
      resultsWritten = true;
      // Primary scored metric is NET correct (correct - incorrect): for a
      // two-choice task this is the unbiased correction for guessing, so
      // rapid random clicking yields ~0 in expectation. Components recorded too.
      QAPI.setED(config.fields.score, correct - incorrect);
      if (config.fields.correct) QAPI.setED(config.fields.correct, correct);
      if (config.fields.incorrect) QAPI.setED(config.fields.incorrect, incorrect);
      QAPI.setED(config.fields.attempted, attempted);
      // Real elapsed seconds, not the configured limit. There is no early-finish
      // path here, so this is normally ~timeLimitSec -- but writing the constant
      // made the column a literal and would have hidden a genuinely short round
      // (an early submit, a reload, a browser that throttled the timer).
      QAPI.setED(config.fields.time, ((now() - startT) / 1000).toFixed(2));
    }
    function finish() {
      if (finished) return;
      finished = true;
      if (timerId) { clearInterval(timerId); timerId = null; }
      timer.textContent = "0s"; timer.classList.remove("low");
      bSame.disabled = true; bMir.disabled = true;
      writeResults();
      msg.innerHTML = "Time is up. You answered <strong>" + correct +
        "</strong> correctly. Click <em>Next</em> to continue.";
      showNext();
    }
    QAPI.onSubmit(writeResults);

    function hideNav() {
      if (q.hideNextButton) q.hideNextButton();
      if (q.hidePreviousButton) q.hidePreviousButton();
      if (window.jQuery) { jQuery("#NextButton").hide(); jQuery("#PreviousButton").hide(); }
    }
    function showNext() {
      if (q.showNextButton) q.showNextButton();
      if (window.jQuery) jQuery("#NextButton").show();
    }

    hideNav();
    newTrial();
    tick();
    timerId = setInterval(tick, 200);
  }

  function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }

  return {
    normalize: normalize, key: key, rotate: rotate, mirror: mirror,
    rotations: rotations, isChiral: isChiral, genPolyomino: genPolyomino,
    genChiral: genChiral, makeTrial: makeTrial, run: run
  };
});

/* Mental rotation — Qualtrics Question JAVASCRIPT (SOURCE; depends on Rotation).
 * Same 3-attempt pattern: the ONLY line that changes across the three copies is
 * `var ATTEMPT = ...;`. Short timer (~45s) keeps the whole study near 20 min. */
(function () {
  var ATTEMPT = 1; // <-- set to 1, 2, or 3 per copy. Nothing else changes.

  Qualtrics.SurveyEngine.addOnload(function () {
    Rotation.run(this, this.getQuestionContainer(), {
      taskId: "rotation",
      attempt: ATTEMPT,
      timeLimitSec: 30,
      shapeSize: 6,   // cells per polyomino
      cellPx: 30,
      fields: {
        score: "rot_a" + ATTEMPT + "_score",        // NET = correct - incorrect (scored metric)
        correct: "rot_a" + ATTEMPT + "_correct",
        incorrect: "rot_a" + ATTEMPT + "_incorrect",
        attempted: "rot_a" + ATTEMPT + "_attempted",
        time: "rot_a" + ATTEMPT + "_time"
      }
    });
  });
})();
Make 3 copies of this question. The only difference between them is the line var ATTEMPT = 1;23. Every field name derives from it, so nothing else changes.
Block 8perceptionnot shown to participants
Create a Qualtrics block named perception holding 2 questions, in the order below.
THE PRIMARY MEASURE. One single Qualtrics question; the JS renders all 17 tasks with their two items each, on one page, with task order and item order randomized per respondent. Kept as one custom-JS question rather than Loop & Merge because the task icons are inline SVG generated in lib/task_icons.js — they cannot travel inside a QSF as image-library URLs, and Loop & Merge would cost 17 page loads instead of 1.
Screen 14 of 22 (the 15 tasks appear in a random order, and so do the two questions within each task — redrawn for every participant)

For each task below, please answer two quick questions about people in general. There are no right or wrong answers; we are only interested in your impressions.

1 of 15
Word search
Finding hidden words in a grid of letters as quickly as possible.
KBTMAP
LNDSEO
RIVERG
ACFHJQ
WXZYUI
PLONTS
find: RIVER
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
2 of 15
Number-string search
Finding a specific string of digits (like a ZIP code) hidden in a grid of digits, as quickly as possible.
937148
205603
482917
150176
726409
391852
find: 90210
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
3 of 15
Number-grid counting
Counting how many numbers in a grid meet a stated condition (for example, greater than 50) and typing the exact count, under time pressure.
1272396418
5123884790
3356217014
4963308227
7511584460
How many are greater than 50?
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
4 of 15
Shape rotation
Deciding whether two flat shapes are the same shape turned around, or mirror images.
vs
same shape, or mirror image?
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
5 of 15
Spelling test
Spelling difficult words correctly.
Spell aloud:
“rhythm”
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
6 of 15
Proofreading
Spotting spelling and grammar errors in a passage of text.
Find the error:
“The quick brown fox jamps over the lazy dog.”
jamps → jumps
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
7 of 15
Digit-symbol coding
Matching symbols to digits using a key, as quickly as possible.
Key
1234
Fill each box using the key:
    
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
8 of 15
Anagrams
Rearranging scrambled letters to form real words.
Unscramble:
P A E L P
P A E L P → APPLE
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
9 of 15
Color-word task
Naming the ink color a word is printed in while ignoring what the word says.
RED  BLUE
Name the ink color (not the word)
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
10 of 15
Mental arithmetic
Adding pairs of two-digit numbers quickly and accurately.
47 + 38 = ?
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
11 of 15
Pattern matrices
Solving abstract visual pattern puzzles by choosing the piece that completes the pattern.
?
which piece completes the pattern?
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
12 of 15
Word generation
Naming as many words as possible that begin with a given letter, as quickly as possible.
Words starting with F:
fish, family, forest, funny…
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
13 of 15
Finger tapping
Tapping a key as many times as possible in a short period.
TAP
tap as fast as you can
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
14 of 15
Emotion recognition
Identifying the emotion someone is feeling from their facial expression.
?
which emotion?
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
15 of 15
Reaction time
Pressing a button as quickly as possible the moment a signal appears.
press the moment it turns green
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
perceptionDescriptive Text (DB) + question JS
Perception module — Qualtrics Question TEXT (HTML view). One question; the engine renders every task with its two items, in randomized order.
Records: perc_<task_id>_top10 and perc_<task_id>_perf for each of the 15 cognitive tasks — 30 fields, perc_task_order, perc_item_order
HTML to paste into the question's HTML View220 bytes
<p style="font-size:15px;">For each task below, please answer two quick questions about people in general.
There are no right or wrong answers; we are only interested in your impressions.</p>

<div id="perc-mount"></div>
JavaScript to paste into the question's JS editor (dist/perception.js)28.9 KB
/* AUTO-GENERATED by build_dist.py — do not edit here. Edit the source
 * (files under lib/ and tasks/) and re-run build_dist.py. */

/* ============================================================================
 * qualtrics_api.js — the ONLY place the task engines touch Qualtrics.
 *
 * Why this file exists
 * --------------------
 * The embedded-data setters and the page-submit hook live on the STATIC
 * `Qualtrics.SurveyEngine` namespace, NOT on the question instance that
 * `this` refers to inside addOnload/addOnReady. Calling them on the instance
 * (`q.setJSEmbeddedData(...)`) throws, and every recorded value is lost — the
 * task renders and plays perfectly, then writes nothing. Guarding the call
 * (`if (q.addOnPageSubmit) ...`) makes that failure silent, which is worse.
 *
 * So: engines call QAPI.setED / QAPI.onSubmit. Those resolve the real static
 * API at call time and THROW LOUDLY if it is not there. A missing API must
 * break the local harness and the Qualtrics preview, not quietly drop data.
 *
 * Layout compatibility
 * --------------------
 * `setJSEmbeddedData` is the current setter and the only one that works in the
 * New Survey Taking Experience; `setEmbeddedData` is deprecated but still works
 * in the legacy layout. We prefer the former and fall back to the latter, so
 * the same code is correct under either layout.
 *
 * Values are coerced to String, and null/undefined become "" rather than the
 * literal text "null"/"undefined".
 * ==========================================================================*/
(function (root) {
  "use strict";

  function engine() {
    return root.Qualtrics && root.Qualtrics.SurveyEngine;
  }

  /* Write one embedded-data field. Throws if no setter is available. */
  function setED(name, value) {
    var E = engine();
    if (!E) {
      throw new Error("QAPI.setED(" + name + "): Qualtrics.SurveyEngine is not available");
    }
    var v = (value === null || value === undefined) ? "" : String(value);
    if (typeof E.setJSEmbeddedData === "function") {
      E.setJSEmbeddedData(name, v);
      return;
    }
    if (typeof E.setEmbeddedData === "function") {
      E.setEmbeddedData(name, v);
      return;
    }
    throw new Error("QAPI.setED(" + name + "): no embedded-data setter on " +
                    "Qualtrics.SurveyEngine — the value would have been lost");
  }

  /* Read one embedded-data field back, with a fallback. Never pipe ${e://...}
   * into JS to do this: piped text is resolved server-side before the page is
   * sent, so it is stale, and a quote in the value is a syntax error. */
  function getED(name, fallback) {
    var E = engine();
    var raw = (E && typeof E.getJSEmbeddedData === "function")
      ? E.getJSEmbeddedData(name) : null;
    return (raw === null || raw === undefined || raw === "") ? fallback : raw;
  }

  /* Register a page-submit handler — where all data writes belong. Data written
   * in addOnUnload races the page POST and is routinely lost. */
  function onSubmit(fn) {
    var E = engine();
    if (!E || typeof E.addOnPageSubmit !== "function") {
      throw new Error("QAPI.onSubmit: Qualtrics.SurveyEngine.addOnPageSubmit is " +
                      "unavailable — results would never be written");
    }
    E.addOnPageSubmit(fn);
  }

  root.QAPI = { setED: setED, getED: getED, onSubmit: onSubmit };
})(typeof window !== "undefined" ? window : this);

/* ============================================================================
 * task_icons.js — task illustrations for the perception module (one per task).
 * Person-free (depict the task artifact, never a person), inline HTML/SVG
 * (self-contained, no external hosting). Sized to be detailed enough that a
 * respondent can imagine actually doing the task. Styling comes from the
 * perception engine's CSS under .perc-img.
 *
 * Dumbbell/lipstick are Material Design Icons (Apache-2.0); see the synthetic
 * study's img/LICENSES.md. To change any image, edit its entry here.
 * ==========================================================================*/
(function (root) {
  "use strict";
  var I = {};

  // --- focal tasks: a mini version of the actual artifact ---

  // 6x6 letter grid, the word RIVER highlighted across the third row.
  I.word_search =
    "<table>" +
    "<tr><td>K</td><td>B</td><td>T</td><td>M</td><td>A</td><td>P</td></tr>" +
    "<tr><td>L</td><td>N</td><td>D</td><td>S</td><td>E</td><td>O</td></tr>" +
    "<tr><td class='hl'>R</td><td class='hl'>I</td><td class='hl'>V</td><td class='hl'>E</td><td class='hl'>R</td><td>G</td></tr>" +
    "<tr><td>A</td><td>C</td><td>F</td><td>H</td><td>J</td><td>Q</td></tr>" +
    "<tr><td>W</td><td>X</td><td>Z</td><td>Y</td><td>U</td><td>I</td></tr>" +
    "<tr><td>P</td><td>L</td><td>O</td><td>N</td><td>T</td><td>S</td></tr></table>" +
    "<div class='cap'>find: RIVER</div>";

  // 6x6 digit grid, the code 90210 highlighted down the diagonal.
  I.zip_search =
    "<table>" +
    "<tr><td class='hl'>9</td><td>3</td><td>7</td><td>1</td><td>4</td><td>8</td></tr>" +
    "<tr><td>2</td><td class='hl'>0</td><td>5</td><td>6</td><td>0</td><td>3</td></tr>" +
    "<tr><td>4</td><td>8</td><td class='hl'>2</td><td>9</td><td>1</td><td>7</td></tr>" +
    "<tr><td>1</td><td>5</td><td>0</td><td class='hl'>1</td><td>7</td><td>6</td></tr>" +
    "<tr><td>7</td><td>2</td><td>6</td><td>4</td><td class='hl'>0</td><td>9</td></tr>" +
    "<tr><td>3</td><td>9</td><td>1</td><td>8</td><td>5</td><td>2</td></tr></table>" +
    "<div class='cap'>find: 90210</div>";

  // 5x5 number grid, values greater than 50 highlighted.
  I.number_grid_count =
    "<table>" +
    "<tr><td class='n'>12</td><td class='n hl'>72</td><td class='n'>39</td><td class='n hl'>64</td><td class='n'>18</td></tr>" +
    "<tr><td class='n hl'>51</td><td class='n'>23</td><td class='n hl'>88</td><td class='n'>47</td><td class='n hl'>90</td></tr>" +
    "<tr><td class='n'>33</td><td class='n hl'>56</td><td class='n'>21</td><td class='n hl'>70</td><td class='n'>14</td></tr>" +
    "<tr><td class='n'>49</td><td class='n hl'>63</td><td class='n'>30</td><td class='n hl'>82</td><td class='n'>27</td></tr>" +
    "<tr><td class='n hl'>75</td><td class='n'>11</td><td class='n hl'>58</td><td class='n'>44</td><td class='n hl'>60</td></tr></table>" +
    "<div class='cap'>How many are greater than 50?</div>";

  // Two shapes: the right one is the left rotated a quarter-turn.
  I.mental_rotation =
    "<svg width='150' height='66' viewBox='-4 -4 150 66'>" +
    "<g fill='#3b6fb0' stroke='#1c3c63' stroke-width='1.5'>" +
    // reference: X. / X. / XX  (cell 18)
    "<rect x='0' y='0' width='18' height='18'/><rect x='0' y='18' width='18' height='18'/>" +
    "<rect x='0' y='36' width='18' height='18'/><rect x='18' y='36' width='18' height='18'/>" +
    // rotated: XXX / X..
    "<rect x='86' y='0' width='18' height='18'/><rect x='104' y='0' width='18' height='18'/>" +
    "<rect x='122' y='0' width='18' height='18'/><rect x='86' y='18' width='18' height='18'/>" +
    "</g><text x='52' y='32' font-size='14' fill='#888'>vs</text></svg>" +
    "<div class='cap'>same shape, or mirror image?</div>";

  // --- anchors: person-free schematics, detailed enough to picture solving ---

  I.spelling = "<div class='word'>Spell aloud:<br><b>&ldquo;rhythm&rdquo;</b> <span style='color:#147a2b'>&#10003;</span></div>";

  I.proofreading =
    "<div class='txt'>Find the error:<br>&ldquo;The quick brown fox <span style='color:#c00'>jamps</span> over the lazy dog.&rdquo;</div>" +
    "<div class='cap'>jamps &rarr; jumps</div>";

  // Digit-symbol coding: a key, then an item to fill in using the key.
  I.dsst =
    "<div class='cap' style='margin-bottom:4px'>Key</div>" +
    "<table><tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>" +
    "<tr><td>&#9675;</td><td>&#9651;</td><td>&#9633;</td><td>&#10005;</td></tr></table>" +
    "<div class='cap' style='margin:8px 0 4px'>Fill each box using the key:</div>" +
    "<table><tr><td>&#9651;</td><td>&#10005;</td><td>&#9675;</td><td>&#9633;</td></tr>" +
    "<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr></table>";

  I.anagrams =
    "<div class='word'>Unscramble:<br><b>P A E L P</b></div>" +
    "<div class='cap'>P A E L P &rarr; APPLE</div>";

  // Stroop: the word's meaning conflicts with its ink color (color IS the task).
  I.stroop =
    "<div class='word' style='font-size:22px;font-weight:700'>" +
    "<span style='color:#1f6feb'>RED</span>&nbsp;&nbsp;<span style='color:#147a2b'>BLUE</span></div>" +
    "<div class='cap'>Name the ink color (not the word)</div>";

  I.arithmetic = "<div class='word' style='font-size:22px'>47 + 38 = ?</div>";

  // Raven's-style 3x3 matrix: dots increase by row+column; bottom-right missing.
  I.ravens =
    "<svg width='150' height='150' viewBox='0 0 114 114'>" +
    "<rect x='0.5' y='0.5' width='113' height='113' fill='#fff' stroke='#bbb'/>" +
    "<g stroke='#ddd'><line x1='38' y1='0' x2='38' y2='114'/><line x1='76' y1='0' x2='76' y2='114'/>" +
    "<line x1='0' y1='38' x2='114' y2='38'/><line x1='0' y1='76' x2='114' y2='76'/></g>" +
    "<g fill='#3b6fb0'>" +
    "<circle cx='19' cy='19' r='3.5'/>" +                                                   // (0,0)=1
    "<circle cx='49' cy='19' r='3.5'/><circle cx='65' cy='19' r='3.5'/>" +                    // (0,1)=2
    "<circle cx='95' cy='10' r='3.5'/><circle cx='87' cy='27' r='3.5'/><circle cx='103' cy='27' r='3.5'/>" + // (0,2)=3
    "<circle cx='11' cy='57' r='3.5'/><circle cx='27' cy='57' r='3.5'/>" +                    // (1,0)=2
    "<circle cx='57' cy='48' r='3.5'/><circle cx='49' cy='65' r='3.5'/><circle cx='65' cy='65' r='3.5'/>" + // (1,1)=3
    "<circle cx='87' cy='49' r='3.5'/><circle cx='103' cy='49' r='3.5'/><circle cx='87' cy='65' r='3.5'/><circle cx='103' cy='65' r='3.5'/>" + // (1,2)=4
    "<circle cx='19' cy='86' r='3.5'/><circle cx='11' cy='103' r='3.5'/><circle cx='27' cy='103' r='3.5'/>" + // (2,0)=3
    "<circle cx='49' cy='87' r='3.5'/><circle cx='65' cy='87' r='3.5'/><circle cx='49' cy='103' r='3.5'/><circle cx='65' cy='103' r='3.5'/>" + // (2,1)=4
    "</g><text x='86' y='104' font-size='26' fill='#999'>?</text></svg>" +
    "<div class='cap'>which piece completes the pattern?</div>";

  // --- sanity extremes: standalone objects (no person) ---
  I.applying_makeup =
    "<svg width='52' height='52' viewBox='0 0 24 24'><path fill='#c24a63' d='M9 23c-.55 0-1-.45-1-1v-9c0-.55.45-1 1-1h6c.55 0 1 .45 1 1v9c0 .55-.45 1-1 1zm1-12c-.55 0-1-.45-1-1V5.25S11 3 11.75 1c1.08.67 2.17 1.33 2.71 2.83S15 7.67 15 10c0 .55-.45 1-1 1z'/></svg>";

  I.lifting_heavy =
    "<svg width='60' height='60' viewBox='0 0 24 24'><path fill='#454552' d='M20.57 14.86L22 13.43L20.57 12L17 15.57L8.43 7L12 3.43L10.57 2L9.14 3.43L7.71 2L5.57 4.14L4.14 2.71L2.71 4.14l1.43 1.43L2 7.71l1.43 1.43L2 10.57L3.43 12L7 8.43L15.57 17L12 20.57L13.43 22l1.43-1.43L16.29 22l2.14-2.14l1.43 1.43l1.43-1.43l-1.43-1.43L22 16.29z'/></svg>";

  // --- added perception tasks ---
  I.verbal_fluency =
    "<div class='word'>Words starting with <b>F</b>:</div>" +
    "<div class='txt' style='text-align:center'>fish, family, forest, funny&hellip;</div>";

  I.finger_tapping =
    "<svg width='80' height='80' viewBox='0 0 80 80'>" +
    "<circle cx='40' cy='40' r='24' fill='#eef4ff' stroke='#1f6feb' stroke-width='3'/>" +
    "<text x='40' y='45' text-anchor='middle' font-size='13' font-weight='700' fill='#1f6feb'>TAP</text>" +
    "</svg><div class='cap'>tap as fast as you can</div>";

  // schematic (genderless) faces: one smiling, one frowning, plus a question mark
  I.emotion_recognition =
    "<svg width='124' height='58' viewBox='0 0 124 58'>" +
    "<g fill='none' stroke='#333' stroke-width='2'><circle cx='22' cy='26' r='18'/><circle cx='92' cy='26' r='18'/></g>" +
    "<g fill='#333'><circle cx='16' cy='21' r='2'/><circle cx='28' cy='21' r='2'/>" +
    "<circle cx='86' cy='21' r='2'/><circle cx='98' cy='21' r='2'/></g>" +
    "<path d='M13 31 Q22 39 31 31' fill='none' stroke='#333' stroke-width='2'/>" +
    "<path d='M83 35 Q92 29 101 35' fill='none' stroke='#333' stroke-width='2'/>" +
    "<text x='57' y='32' font-size='20' fill='#999'>?</text></svg>" +
    "<div class='cap'>which emotion?</div>";

  // a signal light: press when it turns green
  I.reaction_time =
    "<svg width='54' height='96' viewBox='0 0 54 96'>" +
    "<rect x='11' y='4' width='32' height='88' rx='8' fill='#2b2b2b'/>" +
    "<circle cx='27' cy='26' r='11' fill='#7a2620'/>" +
    "<circle cx='27' cy='66' r='11' fill='#27ae60'/>" +
    "</svg><div class='cap'>press the moment it turns green</div>";

  root.TASK_ICONS = I;
})(typeof self !== "undefined" ? self : this);


/* ============================================================================
 * perception_items.js — the gender-typing battery: the task list and the two
 * items. SINGLE SOURCE OF TRUTH.
 *
 * Both perception questions bundle this file, and build_survey_page.py parses it
 * to render the survey draft, so the wording exists exactly once.
 *
 * Two groups, deliberately on TWO Qualtrics pages:
 *
 *   COGNITIVE (15)  the tasks the study is actually about.
 *   ANCHORS   (2)   "Applying makeup" and "Lifting heavy objects" -- extreme
 *                   sanity checks that confirm a respondent will use the full
 *                   range of the scale.
 *
 * The anchors go on a SECOND page, after the cognitive tasks. On one shared page
 * they were visible while the cognitive judgements were being made, and answers
 * near 0 and near 100 act as reference poles that compress everything else
 * toward the middle (range-contrast). The differences among the cognitive tasks
 * are the entire result, so they must not be measured against visible extremes.
 * Putting the anchors last also stops a participant from meeting "Applying
 * makeup" as card 1 and reading the study's purpose off it.
 *
 * Field names are unchanged by the split: both pages use fieldPrefix "perc", so
 * each task still writes perc_<id>_top10 and perc_<id>_perf.
 * ==========================================================================*/
(function (root) {
  "use strict";

  // --- the tasks the study is about -----------------------------------------
  // The four with performance data (here or in the parent studies) come first in
  // source order; presentation order is randomised per respondent and recorded.
  var COGNITIVE = [
    { id: "word_search",       name: "Word search",          description: "Finding hidden words in a grid of letters as quickly as possible." },
    { id: "zip_search",        name: "Number-string search", description: "Finding a specific string of digits (like a ZIP code) hidden in a grid of digits, as quickly as possible." },
    { id: "number_grid_count", name: "Number-grid counting", description: "Counting how many numbers in a grid meet a stated condition (for example, greater than 50) and typing the exact count, under time pressure." },
    // 2-D wording, matching both the task the participant performs and the
    // illustration on this card. It previously said "three-dimensional" to match
    // the synthetic-study instrument, which made the perceived typing of 3-D
    // rotation (a large, well-known stereotype) the comparison for a measured
    // 2-D gap (a much smaller effect) -- the one comparison this study exists
    // to make, and invalid as stated.
    { id: "mental_rotation",   name: "Shape rotation",       description: "Deciding whether two flat shapes are the same shape turned around, or mirror images." },
    { id: "spelling",          name: "Spelling test",        description: "Spelling difficult words correctly." },
    { id: "proofreading",      name: "Proofreading",         description: "Spotting spelling and grammar errors in a passage of text." },
    { id: "dsst",              name: "Digit-symbol coding",  description: "Matching symbols to digits using a key, as quickly as possible." },
    { id: "anagrams",          name: "Anagrams",             description: "Rearranging scrambled letters to form real words." },
    { id: "stroop",            name: "Color-word task",      description: "Naming the ink color a word is printed in while ignoring what the word says." },
    { id: "arithmetic",        name: "Mental arithmetic",    description: "Adding pairs of two-digit numbers quickly and accurately." },
    { id: "ravens",            name: "Pattern matrices",     description: "Solving abstract visual pattern puzzles by choosing the piece that completes the pattern." },
    { id: "verbal_fluency",    name: "Word generation",      description: "Naming as many words as possible that begin with a given letter, as quickly as possible." },
    { id: "finger_tapping",    name: "Finger tapping",       description: "Tapping a key as many times as possible in a short period." },
    { id: "emotion_recognition", name: "Emotion recognition", description: "Identifying the emotion someone is feeling from their facial expression." },
    { id: "reaction_time",     name: "Reaction time",        description: "Pressing a button as quickly as possible the moment a signal appears." }
  ];

  // --- sanity-check extremes, shown last, on their own page ------------------
  var ANCHORS = [
    { id: "applying_makeup",   name: "Applying makeup",      description: "Applying makeup neatly and skillfully." },
    { id: "lifting_heavy",     name: "Lifting heavy objects", description: "Lifting and carrying heavy objects." }
  ];

  // --- the two items asked about every task ---------------------------------
  // Scale directions agree: low = women, midpoint = equal, high = men.
  var ITEMS = {
    top10: {
      id: "top10_men", scaleMin: 0, scaleMax: 100,
      question: "Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?"
    },
    perf: {
      id: "perf_belief", scaleMin: 1, scaleMax: 7,
      question: "On average, who do you think performs better on this task?",
      anchors: { 1: "women perform much better", 4: "men and women perform equally well", 7: "men perform much better" }
    }
  };

  root.PERCEPTION = { COGNITIVE: COGNITIVE, ANCHORS: ANCHORS, ITEMS: ITEMS };
})(typeof window !== "undefined" ? window : this);

/* ============================================================================
 * perception.js — task gender-typing perception module.
 * ----------------------------------------------------------------------------
 * Every respondent rates each task on TWO items, worded identically to the
 * synthetic (LLM) study so the human and LLM rankings compare 1:1:
 *   - Top-decile share (0-100): of the top 10% scorers, how many are men?
 *   - Performance belief (1-7): on average, who performs better?
 *
 * Task order and (within each task) item order are randomized per respondent.
 * One field is written per task per item: perc_<taskid>_top10, perc_<taskid>_perf.
 *
 * Next is hidden until every item is answered (force-response without relying
 * on Qualtrics validation of custom inputs). The 0-100 item is a number entry
 * (no slider) to avoid anchoring on a default handle position.
 * ==========================================================================*/
(function (root, factory) {
  if (typeof module === "object" && module.exports) module.exports = factory();
  else root.Perception = factory();
})(typeof self !== "undefined" ? self : this, function () {
  "use strict";

  function shuffle(a) {
    for (var i = a.length - 1; i > 0; i--) {
      var j = Math.floor(Math.random() * (i + 1));
      var t = a[i]; a[i] = a[j]; a[j] = t;
    }
    return a;
  }

  var CSS_ID = "perc-style";
  var CSS = [
    ".perc-wrap{font-family:Arial,Helvetica,sans-serif;max-width:720px;margin:0 auto;}",
    ".perc-task{border:1px solid #e0e0e0;border-radius:8px;padding:22px 24px;margin:26px 0;background:#fcfcfc;}",
    ".perc-task h4{margin:0 0 4px;font-size:18px;color:#1a1a1a;}",
    ".perc-task .desc{margin:0;font-size:15px;color:#555;}",
    // image sits BELOW the name/description, centered, with room around it
    ".perc-img{margin:20px 0 8px;display:flex;flex-direction:column;align-items:center;}",
    ".perc-img table{border-collapse:collapse;margin:0 auto;}",
    ".perc-img td{border:1px solid #ccc;width:26px;height:26px;text-align:center;font:16px Arial;font-weight:600;color:#333;}",
    ".perc-img td.n{width:34px;font:14px 'Courier New',monospace;}",
    ".perc-img td.hl{background:#c9f3cf;color:#147a2b;}",
    ".perc-img .cap{font:13px Arial;color:#666;text-align:center;margin-top:8px;}",
    ".perc-img .word{font:18px Arial;color:#333;text-align:center;line-height:1.45;}",
    ".perc-img .txt{font:14px Arial;color:#333;text-align:left;line-height:1.5;}",
    ".perc-item{margin:26px 0 6px;}",
    ".perc-item .q{font-size:16px;margin-bottom:14px;line-height:1.4;}",
    ".perc-num{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}",
    ".perc-num input{width:84px;font-size:16px;padding:7px 8px;text-align:center;border:2px solid #bbb;border-radius:4px;}",
    ".perc-num .ends{font-size:13px;color:#888;}",
    // Qualtrics-style scale: a row of options; number on top, radio aligned, anchor text BELOW
    ".perc-likert{display:flex;gap:3px;align-items:flex-start;flex-wrap:wrap;}",
    ".perc-likert label{display:flex;flex-direction:column;align-items:center;font-size:11.5px;color:#555;",
    "width:78px;text-align:center;cursor:pointer;gap:8px;}",
    ".perc-likert input{transform:scale(1.25);cursor:pointer;margin:0;}",
    ".perc-likert .num{font-weight:600;color:#222;font-size:15px;}",
    ".perc-hint{margin:18px 0;font-size:14px;color:#b00;text-align:center;min-height:20px;}"
  ].join("\n");
  function ensureCss() {
    if (document.getElementById(CSS_ID)) return;
    var st = document.createElement("style");
    st.id = CSS_ID; st.type = "text/css";
    st.appendChild(document.createTextNode(CSS));
    document.head.appendChild(st);
  }

  // config: { tasks:[{id,name,description}], items:{top10:{...},perf:{...}},
  //           fieldPrefix:"perc", randomizeTasks:true, randomizeItems:true }
  function run(q, container, config) {
    ensureCss();
    var prefix = config.fieldPrefix || "perc";
    var values = {};                 // "taskid|kind" -> value
    var requiredKeys = [];

    var mount = (container.querySelector && container.querySelector("#perc-mount")) || container;
    mount.innerHTML = "";
    var wrap = el("div", "perc-wrap");

    var tasks = config.tasks.slice();
    if (config.randomizeTasks !== false) shuffle(tasks);
    // The presentation order IS data: without it, a position effect on the primary
    // measure can be neither tested nor controlled for.
    var taskOrder = tasks.map(function (t) { return t.id; });
    var itemOrder = [];

    var icons = config.icons || {};
    tasks.forEach(function (task) {
      var card = el("div", "perc-task");

      // Task name + description, then the person-free image centered below them.
      var h = document.createElement("h4"); h.textContent = task.name; card.appendChild(h);
      var d = el("p", "desc"); d.textContent = task.description; card.appendChild(d);
      var icon = icons[task.id];
      if (icon) {
        var ibox = el("div", "perc-img");
        ibox.innerHTML = icon;
        card.appendChild(ibox);
      }

      // The two items, optionally in randomized order.
      var kinds = ["top10", "perf"];
      if (config.randomizeItems !== false) shuffle(kinds);
      itemOrder.push(task.id + ":" + kinds[0]);
      kinds.forEach(function (kind) {
        card.appendChild(kind === "top10" ? buildNum(task) : buildLikert(task));
      });
      wrap.appendChild(card);
    });

    var hint = el("div", "perc-hint"); hint.id = "perc-hint"; wrap.appendChild(hint);
    mount.appendChild(wrap);

    function buildNum(task) {
      var it = config.items.top10;
      var key = task.id + "|top10";
      requiredKeys.push(key);
      var box = el("div", "perc-item");
      var qd = el("div", "q"); qd.textContent = it.question; box.appendChild(qd);
      var row = el("div", "perc-num");
      var inp = document.createElement("input");
      inp.type = "number"; inp.min = it.scaleMin; inp.max = it.scaleMax; inp.step = 1;
      inp.setAttribute("inputmode", "numeric"); inp.setAttribute("autocomplete", "off");
      // A focused number input takes the mouse wheel and increments itself. On a page
      // this tall, answering a box and then scrolling on to the next card would
      // silently change the answer just given -- invisible corruption of the primary
      // measure. Drop focus rather than let the wheel reach the input.
      inp.addEventListener("wheel", function () {
        if (document.activeElement === inp) inp.blur();
      }, { passive: true });
      var ends = el("span", "ends");
      ends.textContent = "(0 = all women … 50 = half and half … 100 = all men)";
      inp.addEventListener("input", function () {
        var v = inp.value === "" ? null : clamp(parseInt(inp.value, 10), it.scaleMin, it.scaleMax);
        if (v !== null && String(v) !== inp.value) inp.value = v;
        setVal(key, v); refresh();
      });
      row.appendChild(inp); row.appendChild(ends);
      box.appendChild(row);
      return box;
    }

    function buildLikert(task) {
      var it = config.items.perf;
      var key = task.id + "|perf";
      requiredKeys.push(key);
      var box = el("div", "perc-item");
      var qd = el("div", "q"); qd.textContent = it.question; box.appendChild(qd);
      var scale = el("div", "perc-likert");
      var name = prefix + "_" + task.id + "_perf_radio";
      for (var v = it.scaleMin; v <= it.scaleMax; v++) {
        (function (val) {
          var lab = document.createElement("label");
          var num = el("span", "num"); num.textContent = val;
          var inp = document.createElement("input");
          inp.type = "radio"; inp.name = name; inp.value = val;
          var txt = document.createElement("span");
          txt.textContent = it.anchors[val] || "";
          inp.addEventListener("change", function () { setVal(key, val); refresh(); });
          lab.appendChild(num); lab.appendChild(inp); lab.appendChild(txt);
          scale.appendChild(lab);
        })(v);
      }
      box.appendChild(scale);
      return box;
    }

    function setVal(key, v) {
      values[key] = v;
      var parts = key.split("|");
      var field = prefix + "_" + parts[0] + "_" + (parts[1] === "top10" ? "top10" : "perf");
      if (v === null || v === undefined) return;
      QAPI.setED(field, v);
    }
    function complete() {
      for (var i = 0; i < requiredKeys.length; i++) {
        var v = values[requiredKeys[i]];
        if (v === null || v === undefined) return false;
      }
      return true;
    }
    function refresh() {
      if (complete()) { hint.textContent = ""; showNext(); }
      else { hint.textContent = "Please answer every item to continue."; hideNext(); }
    }

    // Flush everything at page submit too (backstop), and record the presentation
    // orders. Field names are configurable so a second copy of this module (the
    // sanity-check anchors, shown on their own page) does not overwrite the first.
    QAPI.onSubmit(function () {
      requiredKeys.forEach(function (k) { setVal(k, values[k]); });
      QAPI.setED(config.orderField || (prefix + "_task_order"), taskOrder.join("|"));
      QAPI.setED(config.itemOrderField || (prefix + "_item_order"), itemOrder.join("|"));
    });

    function hideNext() {
      if (q.hideNextButton) q.hideNextButton();
      if (window.jQuery) jQuery("#NextButton").hide();
    }
    function showNext() {
      if (q.showNextButton) q.showNextButton();
      if (window.jQuery) jQuery("#NextButton").show();
    }

    refresh(); // start with Next hidden until all answered
  }

  function clamp(v, lo, hi) { return isNaN(v) ? null : Math.max(lo, Math.min(hi, v)); }
  function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }

  return { run: run };
})();

/* Perception module, page 1 of 2 — Qualtrics Question JAVASCRIPT (SOURCE).
 * Depends on Perception (lib/perception.js) and PERCEPTION (lib/perception_items.js).
 *
 * The human gender-typing measure is the PRIMARY focus of this study. Item
 * wording is reused from the synthetic-study instrument
 * (experiment/60_task_typing_2026/instruments/perception_items.yaml) because it
 * is clean and neutral, and reusing it lets us compare against the LLM arm as a
 * secondary analysis.
 *
 * This page carries the 15 cognitive tasks. The 2 sanity-check extremes are a
 * SEPARATE question on the next page (perception_anchors_question.js) so their
 * near-0 and near-100 answers cannot act as visible reference poles while these
 * judgements are being made. Edit the task list in lib/perception_items.js.
 *
 * The engine writes one field per task per item (perc_<id>_top10,
 * perc_<id>_perf) plus the presentation orders (perc_task_order,
 * perc_item_order).
 */
(function () {
  Qualtrics.SurveyEngine.addOnload(function () {
    Perception.run(this, this.getQuestionContainer(), {
      tasks: PERCEPTION.COGNITIVE,
      items: PERCEPTION.ITEMS,
      icons: (typeof TASK_ICONS !== "undefined") ? TASK_ICONS : {},
      fieldPrefix: "perc",
      orderField: "perc_task_order",
      itemOrderField: "perc_item_order",
      randomizeTasks: true,
      randomizeItems: true
    });
  });
})();
Screen 15 of 22

Two more, in the same format. Again, there are no right or wrong answers — we are only interested in your impressions of people in general.

1 of 2
Applying makeup
Applying makeup neatly and skillfully.
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
2 of 2
Lifting heavy objects
Lifting and carrying heavy objects.
Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?
of 100 are men
On average, who do you think performs better on this task?
1
women perform much better
2
3
4
men and women perform equally well
5
6
7
men perform much better
perception_anchorsDescriptive Text (DB) + question JS
The two sanity-check extremes, on their own page AFTER the cognitive tasks. They confirm a respondent will use the full range of the scale — if these do not land near the poles, that respondent was not reading. They must not share a page with the cognitive tasks: answers near 0 and near 100 sitting in view act as reference poles and compress everything else toward the middle, and the differences among the cognitive tasks are the entire result.
Perception module, page 2 of 2 - Qualtrics Question TEXT (HTML view). The two sanity-check extremes, on their own page after the cognitive tasks.
Records: perc_applying_makeup_top10 / _perf, perc_lifting_heavy_top10 / _perf, perc_anchor_order, perc_anchor_item_order
HTML to paste into the question's HTML View205 bytes
<p style="font-size:15px;">Two more, in the same format. Again, there are no right or wrong
answers &mdash; we are only interested in your impressions of people in general.</p>

<div id="perc-mount"></div>
JavaScript to paste into the question's JS editor (dist/perception_anchors.js)28.7 KB
/* AUTO-GENERATED by build_dist.py — do not edit here. Edit the source
 * (files under lib/ and tasks/) and re-run build_dist.py. */

/* ============================================================================
 * qualtrics_api.js — the ONLY place the task engines touch Qualtrics.
 *
 * Why this file exists
 * --------------------
 * The embedded-data setters and the page-submit hook live on the STATIC
 * `Qualtrics.SurveyEngine` namespace, NOT on the question instance that
 * `this` refers to inside addOnload/addOnReady. Calling them on the instance
 * (`q.setJSEmbeddedData(...)`) throws, and every recorded value is lost — the
 * task renders and plays perfectly, then writes nothing. Guarding the call
 * (`if (q.addOnPageSubmit) ...`) makes that failure silent, which is worse.
 *
 * So: engines call QAPI.setED / QAPI.onSubmit. Those resolve the real static
 * API at call time and THROW LOUDLY if it is not there. A missing API must
 * break the local harness and the Qualtrics preview, not quietly drop data.
 *
 * Layout compatibility
 * --------------------
 * `setJSEmbeddedData` is the current setter and the only one that works in the
 * New Survey Taking Experience; `setEmbeddedData` is deprecated but still works
 * in the legacy layout. We prefer the former and fall back to the latter, so
 * the same code is correct under either layout.
 *
 * Values are coerced to String, and null/undefined become "" rather than the
 * literal text "null"/"undefined".
 * ==========================================================================*/
(function (root) {
  "use strict";

  function engine() {
    return root.Qualtrics && root.Qualtrics.SurveyEngine;
  }

  /* Write one embedded-data field. Throws if no setter is available. */
  function setED(name, value) {
    var E = engine();
    if (!E) {
      throw new Error("QAPI.setED(" + name + "): Qualtrics.SurveyEngine is not available");
    }
    var v = (value === null || value === undefined) ? "" : String(value);
    if (typeof E.setJSEmbeddedData === "function") {
      E.setJSEmbeddedData(name, v);
      return;
    }
    if (typeof E.setEmbeddedData === "function") {
      E.setEmbeddedData(name, v);
      return;
    }
    throw new Error("QAPI.setED(" + name + "): no embedded-data setter on " +
                    "Qualtrics.SurveyEngine — the value would have been lost");
  }

  /* Read one embedded-data field back, with a fallback. Never pipe ${e://...}
   * into JS to do this: piped text is resolved server-side before the page is
   * sent, so it is stale, and a quote in the value is a syntax error. */
  function getED(name, fallback) {
    var E = engine();
    var raw = (E && typeof E.getJSEmbeddedData === "function")
      ? E.getJSEmbeddedData(name) : null;
    return (raw === null || raw === undefined || raw === "") ? fallback : raw;
  }

  /* Register a page-submit handler — where all data writes belong. Data written
   * in addOnUnload races the page POST and is routinely lost. */
  function onSubmit(fn) {
    var E = engine();
    if (!E || typeof E.addOnPageSubmit !== "function") {
      throw new Error("QAPI.onSubmit: Qualtrics.SurveyEngine.addOnPageSubmit is " +
                      "unavailable — results would never be written");
    }
    E.addOnPageSubmit(fn);
  }

  root.QAPI = { setED: setED, getED: getED, onSubmit: onSubmit };
})(typeof window !== "undefined" ? window : this);

/* ============================================================================
 * task_icons.js — task illustrations for the perception module (one per task).
 * Person-free (depict the task artifact, never a person), inline HTML/SVG
 * (self-contained, no external hosting). Sized to be detailed enough that a
 * respondent can imagine actually doing the task. Styling comes from the
 * perception engine's CSS under .perc-img.
 *
 * Dumbbell/lipstick are Material Design Icons (Apache-2.0); see the synthetic
 * study's img/LICENSES.md. To change any image, edit its entry here.
 * ==========================================================================*/
(function (root) {
  "use strict";
  var I = {};

  // --- focal tasks: a mini version of the actual artifact ---

  // 6x6 letter grid, the word RIVER highlighted across the third row.
  I.word_search =
    "<table>" +
    "<tr><td>K</td><td>B</td><td>T</td><td>M</td><td>A</td><td>P</td></tr>" +
    "<tr><td>L</td><td>N</td><td>D</td><td>S</td><td>E</td><td>O</td></tr>" +
    "<tr><td class='hl'>R</td><td class='hl'>I</td><td class='hl'>V</td><td class='hl'>E</td><td class='hl'>R</td><td>G</td></tr>" +
    "<tr><td>A</td><td>C</td><td>F</td><td>H</td><td>J</td><td>Q</td></tr>" +
    "<tr><td>W</td><td>X</td><td>Z</td><td>Y</td><td>U</td><td>I</td></tr>" +
    "<tr><td>P</td><td>L</td><td>O</td><td>N</td><td>T</td><td>S</td></tr></table>" +
    "<div class='cap'>find: RIVER</div>";

  // 6x6 digit grid, the code 90210 highlighted down the diagonal.
  I.zip_search =
    "<table>" +
    "<tr><td class='hl'>9</td><td>3</td><td>7</td><td>1</td><td>4</td><td>8</td></tr>" +
    "<tr><td>2</td><td class='hl'>0</td><td>5</td><td>6</td><td>0</td><td>3</td></tr>" +
    "<tr><td>4</td><td>8</td><td class='hl'>2</td><td>9</td><td>1</td><td>7</td></tr>" +
    "<tr><td>1</td><td>5</td><td>0</td><td class='hl'>1</td><td>7</td><td>6</td></tr>" +
    "<tr><td>7</td><td>2</td><td>6</td><td>4</td><td class='hl'>0</td><td>9</td></tr>" +
    "<tr><td>3</td><td>9</td><td>1</td><td>8</td><td>5</td><td>2</td></tr></table>" +
    "<div class='cap'>find: 90210</div>";

  // 5x5 number grid, values greater than 50 highlighted.
  I.number_grid_count =
    "<table>" +
    "<tr><td class='n'>12</td><td class='n hl'>72</td><td class='n'>39</td><td class='n hl'>64</td><td class='n'>18</td></tr>" +
    "<tr><td class='n hl'>51</td><td class='n'>23</td><td class='n hl'>88</td><td class='n'>47</td><td class='n hl'>90</td></tr>" +
    "<tr><td class='n'>33</td><td class='n hl'>56</td><td class='n'>21</td><td class='n hl'>70</td><td class='n'>14</td></tr>" +
    "<tr><td class='n'>49</td><td class='n hl'>63</td><td class='n'>30</td><td class='n hl'>82</td><td class='n'>27</td></tr>" +
    "<tr><td class='n hl'>75</td><td class='n'>11</td><td class='n hl'>58</td><td class='n'>44</td><td class='n hl'>60</td></tr></table>" +
    "<div class='cap'>How many are greater than 50?</div>";

  // Two shapes: the right one is the left rotated a quarter-turn.
  I.mental_rotation =
    "<svg width='150' height='66' viewBox='-4 -4 150 66'>" +
    "<g fill='#3b6fb0' stroke='#1c3c63' stroke-width='1.5'>" +
    // reference: X. / X. / XX  (cell 18)
    "<rect x='0' y='0' width='18' height='18'/><rect x='0' y='18' width='18' height='18'/>" +
    "<rect x='0' y='36' width='18' height='18'/><rect x='18' y='36' width='18' height='18'/>" +
    // rotated: XXX / X..
    "<rect x='86' y='0' width='18' height='18'/><rect x='104' y='0' width='18' height='18'/>" +
    "<rect x='122' y='0' width='18' height='18'/><rect x='86' y='18' width='18' height='18'/>" +
    "</g><text x='52' y='32' font-size='14' fill='#888'>vs</text></svg>" +
    "<div class='cap'>same shape, or mirror image?</div>";

  // --- anchors: person-free schematics, detailed enough to picture solving ---

  I.spelling = "<div class='word'>Spell aloud:<br><b>&ldquo;rhythm&rdquo;</b> <span style='color:#147a2b'>&#10003;</span></div>";

  I.proofreading =
    "<div class='txt'>Find the error:<br>&ldquo;The quick brown fox <span style='color:#c00'>jamps</span> over the lazy dog.&rdquo;</div>" +
    "<div class='cap'>jamps &rarr; jumps</div>";

  // Digit-symbol coding: a key, then an item to fill in using the key.
  I.dsst =
    "<div class='cap' style='margin-bottom:4px'>Key</div>" +
    "<table><tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>" +
    "<tr><td>&#9675;</td><td>&#9651;</td><td>&#9633;</td><td>&#10005;</td></tr></table>" +
    "<div class='cap' style='margin:8px 0 4px'>Fill each box using the key:</div>" +
    "<table><tr><td>&#9651;</td><td>&#10005;</td><td>&#9675;</td><td>&#9633;</td></tr>" +
    "<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr></table>";

  I.anagrams =
    "<div class='word'>Unscramble:<br><b>P A E L P</b></div>" +
    "<div class='cap'>P A E L P &rarr; APPLE</div>";

  // Stroop: the word's meaning conflicts with its ink color (color IS the task).
  I.stroop =
    "<div class='word' style='font-size:22px;font-weight:700'>" +
    "<span style='color:#1f6feb'>RED</span>&nbsp;&nbsp;<span style='color:#147a2b'>BLUE</span></div>" +
    "<div class='cap'>Name the ink color (not the word)</div>";

  I.arithmetic = "<div class='word' style='font-size:22px'>47 + 38 = ?</div>";

  // Raven's-style 3x3 matrix: dots increase by row+column; bottom-right missing.
  I.ravens =
    "<svg width='150' height='150' viewBox='0 0 114 114'>" +
    "<rect x='0.5' y='0.5' width='113' height='113' fill='#fff' stroke='#bbb'/>" +
    "<g stroke='#ddd'><line x1='38' y1='0' x2='38' y2='114'/><line x1='76' y1='0' x2='76' y2='114'/>" +
    "<line x1='0' y1='38' x2='114' y2='38'/><line x1='0' y1='76' x2='114' y2='76'/></g>" +
    "<g fill='#3b6fb0'>" +
    "<circle cx='19' cy='19' r='3.5'/>" +                                                   // (0,0)=1
    "<circle cx='49' cy='19' r='3.5'/><circle cx='65' cy='19' r='3.5'/>" +                    // (0,1)=2
    "<circle cx='95' cy='10' r='3.5'/><circle cx='87' cy='27' r='3.5'/><circle cx='103' cy='27' r='3.5'/>" + // (0,2)=3
    "<circle cx='11' cy='57' r='3.5'/><circle cx='27' cy='57' r='3.5'/>" +                    // (1,0)=2
    "<circle cx='57' cy='48' r='3.5'/><circle cx='49' cy='65' r='3.5'/><circle cx='65' cy='65' r='3.5'/>" + // (1,1)=3
    "<circle cx='87' cy='49' r='3.5'/><circle cx='103' cy='49' r='3.5'/><circle cx='87' cy='65' r='3.5'/><circle cx='103' cy='65' r='3.5'/>" + // (1,2)=4
    "<circle cx='19' cy='86' r='3.5'/><circle cx='11' cy='103' r='3.5'/><circle cx='27' cy='103' r='3.5'/>" + // (2,0)=3
    "<circle cx='49' cy='87' r='3.5'/><circle cx='65' cy='87' r='3.5'/><circle cx='49' cy='103' r='3.5'/><circle cx='65' cy='103' r='3.5'/>" + // (2,1)=4
    "</g><text x='86' y='104' font-size='26' fill='#999'>?</text></svg>" +
    "<div class='cap'>which piece completes the pattern?</div>";

  // --- sanity extremes: standalone objects (no person) ---
  I.applying_makeup =
    "<svg width='52' height='52' viewBox='0 0 24 24'><path fill='#c24a63' d='M9 23c-.55 0-1-.45-1-1v-9c0-.55.45-1 1-1h6c.55 0 1 .45 1 1v9c0 .55-.45 1-1 1zm1-12c-.55 0-1-.45-1-1V5.25S11 3 11.75 1c1.08.67 2.17 1.33 2.71 2.83S15 7.67 15 10c0 .55-.45 1-1 1z'/></svg>";

  I.lifting_heavy =
    "<svg width='60' height='60' viewBox='0 0 24 24'><path fill='#454552' d='M20.57 14.86L22 13.43L20.57 12L17 15.57L8.43 7L12 3.43L10.57 2L9.14 3.43L7.71 2L5.57 4.14L4.14 2.71L2.71 4.14l1.43 1.43L2 7.71l1.43 1.43L2 10.57L3.43 12L7 8.43L15.57 17L12 20.57L13.43 22l1.43-1.43L16.29 22l2.14-2.14l1.43 1.43l1.43-1.43l-1.43-1.43L22 16.29z'/></svg>";

  // --- added perception tasks ---
  I.verbal_fluency =
    "<div class='word'>Words starting with <b>F</b>:</div>" +
    "<div class='txt' style='text-align:center'>fish, family, forest, funny&hellip;</div>";

  I.finger_tapping =
    "<svg width='80' height='80' viewBox='0 0 80 80'>" +
    "<circle cx='40' cy='40' r='24' fill='#eef4ff' stroke='#1f6feb' stroke-width='3'/>" +
    "<text x='40' y='45' text-anchor='middle' font-size='13' font-weight='700' fill='#1f6feb'>TAP</text>" +
    "</svg><div class='cap'>tap as fast as you can</div>";

  // schematic (genderless) faces: one smiling, one frowning, plus a question mark
  I.emotion_recognition =
    "<svg width='124' height='58' viewBox='0 0 124 58'>" +
    "<g fill='none' stroke='#333' stroke-width='2'><circle cx='22' cy='26' r='18'/><circle cx='92' cy='26' r='18'/></g>" +
    "<g fill='#333'><circle cx='16' cy='21' r='2'/><circle cx='28' cy='21' r='2'/>" +
    "<circle cx='86' cy='21' r='2'/><circle cx='98' cy='21' r='2'/></g>" +
    "<path d='M13 31 Q22 39 31 31' fill='none' stroke='#333' stroke-width='2'/>" +
    "<path d='M83 35 Q92 29 101 35' fill='none' stroke='#333' stroke-width='2'/>" +
    "<text x='57' y='32' font-size='20' fill='#999'>?</text></svg>" +
    "<div class='cap'>which emotion?</div>";

  // a signal light: press when it turns green
  I.reaction_time =
    "<svg width='54' height='96' viewBox='0 0 54 96'>" +
    "<rect x='11' y='4' width='32' height='88' rx='8' fill='#2b2b2b'/>" +
    "<circle cx='27' cy='26' r='11' fill='#7a2620'/>" +
    "<circle cx='27' cy='66' r='11' fill='#27ae60'/>" +
    "</svg><div class='cap'>press the moment it turns green</div>";

  root.TASK_ICONS = I;
})(typeof self !== "undefined" ? self : this);


/* ============================================================================
 * perception_items.js — the gender-typing battery: the task list and the two
 * items. SINGLE SOURCE OF TRUTH.
 *
 * Both perception questions bundle this file, and build_survey_page.py parses it
 * to render the survey draft, so the wording exists exactly once.
 *
 * Two groups, deliberately on TWO Qualtrics pages:
 *
 *   COGNITIVE (15)  the tasks the study is actually about.
 *   ANCHORS   (2)   "Applying makeup" and "Lifting heavy objects" -- extreme
 *                   sanity checks that confirm a respondent will use the full
 *                   range of the scale.
 *
 * The anchors go on a SECOND page, after the cognitive tasks. On one shared page
 * they were visible while the cognitive judgements were being made, and answers
 * near 0 and near 100 act as reference poles that compress everything else
 * toward the middle (range-contrast). The differences among the cognitive tasks
 * are the entire result, so they must not be measured against visible extremes.
 * Putting the anchors last also stops a participant from meeting "Applying
 * makeup" as card 1 and reading the study's purpose off it.
 *
 * Field names are unchanged by the split: both pages use fieldPrefix "perc", so
 * each task still writes perc_<id>_top10 and perc_<id>_perf.
 * ==========================================================================*/
(function (root) {
  "use strict";

  // --- the tasks the study is about -----------------------------------------
  // The four with performance data (here or in the parent studies) come first in
  // source order; presentation order is randomised per respondent and recorded.
  var COGNITIVE = [
    { id: "word_search",       name: "Word search",          description: "Finding hidden words in a grid of letters as quickly as possible." },
    { id: "zip_search",        name: "Number-string search", description: "Finding a specific string of digits (like a ZIP code) hidden in a grid of digits, as quickly as possible." },
    { id: "number_grid_count", name: "Number-grid counting", description: "Counting how many numbers in a grid meet a stated condition (for example, greater than 50) and typing the exact count, under time pressure." },
    // 2-D wording, matching both the task the participant performs and the
    // illustration on this card. It previously said "three-dimensional" to match
    // the synthetic-study instrument, which made the perceived typing of 3-D
    // rotation (a large, well-known stereotype) the comparison for a measured
    // 2-D gap (a much smaller effect) -- the one comparison this study exists
    // to make, and invalid as stated.
    { id: "mental_rotation",   name: "Shape rotation",       description: "Deciding whether two flat shapes are the same shape turned around, or mirror images." },
    { id: "spelling",          name: "Spelling test",        description: "Spelling difficult words correctly." },
    { id: "proofreading",      name: "Proofreading",         description: "Spotting spelling and grammar errors in a passage of text." },
    { id: "dsst",              name: "Digit-symbol coding",  description: "Matching symbols to digits using a key, as quickly as possible." },
    { id: "anagrams",          name: "Anagrams",             description: "Rearranging scrambled letters to form real words." },
    { id: "stroop",            name: "Color-word task",      description: "Naming the ink color a word is printed in while ignoring what the word says." },
    { id: "arithmetic",        name: "Mental arithmetic",    description: "Adding pairs of two-digit numbers quickly and accurately." },
    { id: "ravens",            name: "Pattern matrices",     description: "Solving abstract visual pattern puzzles by choosing the piece that completes the pattern." },
    { id: "verbal_fluency",    name: "Word generation",      description: "Naming as many words as possible that begin with a given letter, as quickly as possible." },
    { id: "finger_tapping",    name: "Finger tapping",       description: "Tapping a key as many times as possible in a short period." },
    { id: "emotion_recognition", name: "Emotion recognition", description: "Identifying the emotion someone is feeling from their facial expression." },
    { id: "reaction_time",     name: "Reaction time",        description: "Pressing a button as quickly as possible the moment a signal appears." }
  ];

  // --- sanity-check extremes, shown last, on their own page ------------------
  var ANCHORS = [
    { id: "applying_makeup",   name: "Applying makeup",      description: "Applying makeup neatly and skillfully." },
    { id: "lifting_heavy",     name: "Lifting heavy objects", description: "Lifting and carrying heavy objects." }
  ];

  // --- the two items asked about every task ---------------------------------
  // Scale directions agree: low = women, midpoint = equal, high = men.
  var ITEMS = {
    top10: {
      id: "top10_men", scaleMin: 0, scaleMax: 100,
      question: "Imagine the 100 people who score in the very top 10% on this task. How many of those 100 people do you think are men?"
    },
    perf: {
      id: "perf_belief", scaleMin: 1, scaleMax: 7,
      question: "On average, who do you think performs better on this task?",
      anchors: { 1: "women perform much better", 4: "men and women perform equally well", 7: "men perform much better" }
    }
  };

  root.PERCEPTION = { COGNITIVE: COGNITIVE, ANCHORS: ANCHORS, ITEMS: ITEMS };
})(typeof window !== "undefined" ? window : this);

/* ============================================================================
 * perception.js — task gender-typing perception module.
 * ----------------------------------------------------------------------------
 * Every respondent rates each task on TWO items, worded identically to the
 * synthetic (LLM) study so the human and LLM rankings compare 1:1:
 *   - Top-decile share (0-100): of the top 10% scorers, how many are men?
 *   - Performance belief (1-7): on average, who performs better?
 *
 * Task order and (within each task) item order are randomized per respondent.
 * One field is written per task per item: perc_<taskid>_top10, perc_<taskid>_perf.
 *
 * Next is hidden until every item is answered (force-response without relying
 * on Qualtrics validation of custom inputs). The 0-100 item is a number entry
 * (no slider) to avoid anchoring on a default handle position.
 * ==========================================================================*/
(function (root, factory) {
  if (typeof module === "object" && module.exports) module.exports = factory();
  else root.Perception = factory();
})(typeof self !== "undefined" ? self : this, function () {
  "use strict";

  function shuffle(a) {
    for (var i = a.length - 1; i > 0; i--) {
      var j = Math.floor(Math.random() * (i + 1));
      var t = a[i]; a[i] = a[j]; a[j] = t;
    }
    return a;
  }

  var CSS_ID = "perc-style";
  var CSS = [
    ".perc-wrap{font-family:Arial,Helvetica,sans-serif;max-width:720px;margin:0 auto;}",
    ".perc-task{border:1px solid #e0e0e0;border-radius:8px;padding:22px 24px;margin:26px 0;background:#fcfcfc;}",
    ".perc-task h4{margin:0 0 4px;font-size:18px;color:#1a1a1a;}",
    ".perc-task .desc{margin:0;font-size:15px;color:#555;}",
    // image sits BELOW the name/description, centered, with room around it
    ".perc-img{margin:20px 0 8px;display:flex;flex-direction:column;align-items:center;}",
    ".perc-img table{border-collapse:collapse;margin:0 auto;}",
    ".perc-img td{border:1px solid #ccc;width:26px;height:26px;text-align:center;font:16px Arial;font-weight:600;color:#333;}",
    ".perc-img td.n{width:34px;font:14px 'Courier New',monospace;}",
    ".perc-img td.hl{background:#c9f3cf;color:#147a2b;}",
    ".perc-img .cap{font:13px Arial;color:#666;text-align:center;margin-top:8px;}",
    ".perc-img .word{font:18px Arial;color:#333;text-align:center;line-height:1.45;}",
    ".perc-img .txt{font:14px Arial;color:#333;text-align:left;line-height:1.5;}",
    ".perc-item{margin:26px 0 6px;}",
    ".perc-item .q{font-size:16px;margin-bottom:14px;line-height:1.4;}",
    ".perc-num{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}",
    ".perc-num input{width:84px;font-size:16px;padding:7px 8px;text-align:center;border:2px solid #bbb;border-radius:4px;}",
    ".perc-num .ends{font-size:13px;color:#888;}",
    // Qualtrics-style scale: a row of options; number on top, radio aligned, anchor text BELOW
    ".perc-likert{display:flex;gap:3px;align-items:flex-start;flex-wrap:wrap;}",
    ".perc-likert label{display:flex;flex-direction:column;align-items:center;font-size:11.5px;color:#555;",
    "width:78px;text-align:center;cursor:pointer;gap:8px;}",
    ".perc-likert input{transform:scale(1.25);cursor:pointer;margin:0;}",
    ".perc-likert .num{font-weight:600;color:#222;font-size:15px;}",
    ".perc-hint{margin:18px 0;font-size:14px;color:#b00;text-align:center;min-height:20px;}"
  ].join("\n");
  function ensureCss() {
    if (document.getElementById(CSS_ID)) return;
    var st = document.createElement("style");
    st.id = CSS_ID; st.type = "text/css";
    st.appendChild(document.createTextNode(CSS));
    document.head.appendChild(st);
  }

  // config: { tasks:[{id,name,description}], items:{top10:{...},perf:{...}},
  //           fieldPrefix:"perc", randomizeTasks:true, randomizeItems:true }
  function run(q, container, config) {
    ensureCss();
    var prefix = config.fieldPrefix || "perc";
    var values = {};                 // "taskid|kind" -> value
    var requiredKeys = [];

    var mount = (container.querySelector && container.querySelector("#perc-mount")) || container;
    mount.innerHTML = "";
    var wrap = el("div", "perc-wrap");

    var tasks = config.tasks.slice();
    if (config.randomizeTasks !== false) shuffle(tasks);
    // The presentation order IS data: without it, a position effect on the primary
    // measure can be neither tested nor controlled for.
    var taskOrder = tasks.map(function (t) { return t.id; });
    var itemOrder = [];

    var icons = config.icons || {};
    tasks.forEach(function (task) {
      var card = el("div", "perc-task");

      // Task name + description, then the person-free image centered below them.
      var h = document.createElement("h4"); h.textContent = task.name; card.appendChild(h);
      var d = el("p", "desc"); d.textContent = task.description; card.appendChild(d);
      var icon = icons[task.id];
      if (icon) {
        var ibox = el("div", "perc-img");
        ibox.innerHTML = icon;
        card.appendChild(ibox);
      }

      // The two items, optionally in randomized order.
      var kinds = ["top10", "perf"];
      if (config.randomizeItems !== false) shuffle(kinds);
      itemOrder.push(task.id + ":" + kinds[0]);
      kinds.forEach(function (kind) {
        card.appendChild(kind === "top10" ? buildNum(task) : buildLikert(task));
      });
      wrap.appendChild(card);
    });

    var hint = el("div", "perc-hint"); hint.id = "perc-hint"; wrap.appendChild(hint);
    mount.appendChild(wrap);

    function buildNum(task) {
      var it = config.items.top10;
      var key = task.id + "|top10";
      requiredKeys.push(key);
      var box = el("div", "perc-item");
      var qd = el("div", "q"); qd.textContent = it.question; box.appendChild(qd);
      var row = el("div", "perc-num");
      var inp = document.createElement("input");
      inp.type = "number"; inp.min = it.scaleMin; inp.max = it.scaleMax; inp.step = 1;
      inp.setAttribute("inputmode", "numeric"); inp.setAttribute("autocomplete", "off");
      // A focused number input takes the mouse wheel and increments itself. On a page
      // this tall, answering a box and then scrolling on to the next card would
      // silently change the answer just given -- invisible corruption of the primary
      // measure. Drop focus rather than let the wheel reach the input.
      inp.addEventListener("wheel", function () {
        if (document.activeElement === inp) inp.blur();
      }, { passive: true });
      var ends = el("span", "ends");
      ends.textContent = "(0 = all women … 50 = half and half … 100 = all men)";
      inp.addEventListener("input", function () {
        var v = inp.value === "" ? null : clamp(parseInt(inp.value, 10), it.scaleMin, it.scaleMax);
        if (v !== null && String(v) !== inp.value) inp.value = v;
        setVal(key, v); refresh();
      });
      row.appendChild(inp); row.appendChild(ends);
      box.appendChild(row);
      return box;
    }

    function buildLikert(task) {
      var it = config.items.perf;
      var key = task.id + "|perf";
      requiredKeys.push(key);
      var box = el("div", "perc-item");
      var qd = el("div", "q"); qd.textContent = it.question; box.appendChild(qd);
      var scale = el("div", "perc-likert");
      var name = prefix + "_" + task.id + "_perf_radio";
      for (var v = it.scaleMin; v <= it.scaleMax; v++) {
        (function (val) {
          var lab = document.createElement("label");
          var num = el("span", "num"); num.textContent = val;
          var inp = document.createElement("input");
          inp.type = "radio"; inp.name = name; inp.value = val;
          var txt = document.createElement("span");
          txt.textContent = it.anchors[val] || "";
          inp.addEventListener("change", function () { setVal(key, val); refresh(); });
          lab.appendChild(num); lab.appendChild(inp); lab.appendChild(txt);
          scale.appendChild(lab);
        })(v);
      }
      box.appendChild(scale);
      return box;
    }

    function setVal(key, v) {
      values[key] = v;
      var parts = key.split("|");
      var field = prefix + "_" + parts[0] + "_" + (parts[1] === "top10" ? "top10" : "perf");
      if (v === null || v === undefined) return;
      QAPI.setED(field, v);
    }
    function complete() {
      for (var i = 0; i < requiredKeys.length; i++) {
        var v = values[requiredKeys[i]];
        if (v === null || v === undefined) return false;
      }
      return true;
    }
    function refresh() {
      if (complete()) { hint.textContent = ""; showNext(); }
      else { hint.textContent = "Please answer every item to continue."; hideNext(); }
    }

    // Flush everything at page submit too (backstop), and record the presentation
    // orders. Field names are configurable so a second copy of this module (the
    // sanity-check anchors, shown on their own page) does not overwrite the first.
    QAPI.onSubmit(function () {
      requiredKeys.forEach(function (k) { setVal(k, values[k]); });
      QAPI.setED(config.orderField || (prefix + "_task_order"), taskOrder.join("|"));
      QAPI.setED(config.itemOrderField || (prefix + "_item_order"), itemOrder.join("|"));
    });

    function hideNext() {
      if (q.hideNextButton) q.hideNextButton();
      if (window.jQuery) jQuery("#NextButton").hide();
    }
    function showNext() {
      if (q.showNextButton) q.showNextButton();
      if (window.jQuery) jQuery("#NextButton").show();
    }

    refresh(); // start with Next hidden until all answered
  }

  function clamp(v, lo, hi) { return isNaN(v) ? null : Math.max(lo, Math.min(hi, v)); }
  function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }

  return { run: run };
})();

/* Perception module, page 2 of 2 — Qualtrics Question JAVASCRIPT (SOURCE).
 * Depends on Perception (lib/perception.js) and PERCEPTION (lib/perception_items.js).
 *
 * The two sanity-check extremes ("Applying makeup", "Lifting heavy objects").
 * They exist to confirm a respondent will use the full range of the scale: if
 * these two do not come out near the poles, that respondent was not reading.
 *
 * They sit on their own page, AFTER the 15 cognitive tasks, so their extreme
 * answers cannot serve as visible reference poles that compress the cognitive
 * judgements toward the middle. Same engine, same items, same field prefix --
 * each task still writes perc_<id>_top10 and perc_<id>_perf -- only the order
 * fields differ, so this page cannot overwrite page 1's.
 *
 * Edit the anchor list in lib/perception_items.js.
 */
(function () {
  Qualtrics.SurveyEngine.addOnload(function () {
    Perception.run(this, this.getQuestionContainer(), {
      tasks: PERCEPTION.ANCHORS,
      items: PERCEPTION.ITEMS,
      icons: (typeof TASK_ICONS !== "undefined") ? TASK_ICONS : {},
      fieldPrefix: "perc",
      orderField: "perc_anchor_order",
      itemOrderField: "perc_anchor_item_order",
      randomizeTasks: true,
      randomizeItems: true
    });
  });
})();
Block 9demographicsnot shown to participants
Create a Qualtrics block named demographics holding 6 questions, in the order below.
LAST, so nothing raises gender before the tasks or the perception items. Wording and answer choices for gender, age, ethnicity and education are reused VERBATIM from the parent Ambition studies so the samples pool. Income is the one deliberate change — see its note.
Screen 16 of 22

How much do you agree with each of the following?

Strongly disagree Disagree Neither agree nor disagree Agree Strongly agree
I am generally willing to take risks.
I enjoy competing against other people.
To show you are reading carefully, please select “Strongly disagree” for this item.
When I face a difficult task, I am certain that I will accomplish it.
traitsMatrix Table (Likert, 5-point)Force Response
Three trait measures plus the attention check. Rows 1, 2 and 4 are self-assessed risk tolerance, competitiveness and confidence — the three traits the gender-economics literature uses to explain gender gaps in labour-market outcomes. Wording adapted from Dohmen et al. 2011 (risk), Smither & Houston 1992 (competitiveness) and Chen, Gully & Eden 2001 (self-efficacy). They replace three filler statements that looked like persistence measures but were recorded by nothing.
Row 3 is an instructed-response attention check (Meade & Craig 2012; the trap-paragraph design of Oppenheimer, Meyvis & Davidenko 2009 is a different instrument). Record it with TWO Survey Flow branches, not JS: one setting attn_pass = 1 when row 3 is answered “Strongly disagree”, one setting it to 0 otherwise. Both are needed — with only the pass branch, a genuine failure is indistinguishable from a break-off before the grid. The field is named for what 1 means, because a flag whose polarity has to be looked up is how an exclusion gets inverted at analysis time. The instructed answer is the scale endpoint, not “Disagree”: a satisficer who disagrees with everything would otherwise pass by luck, and would now also be corrupting three real measures. Put the grid and the branches in separate blocks, declare the field before them, and pre-register the exclusion.
Limitation — these are measured post-treatment. This screen comes after three scored timed tasks and the whole gender-typing battery, so competitiveness and especially confidence are contaminated by how the participant just performed: someone who found 1 of 5 codes will report lower confidence than the same person would have beforehand. Usable descriptively and as outcomes; not clean as traits or as controls. Moving them earlier is not a fix — it would prime exactly what the ordering is designed to avoid. Pre-register them as secondary, and check them against task score.
Records: traits_1 risk · traits_2 competitiveness · traits_4 confidence (ordinary matrix columns, one per row — not embedded data), attn_pass — 1 = passed, 0 = failed, blank = never reached the grid (set by two Survey Flow branches on row 3)
HTML to paste into the question's HTML View56 bytes
<p>How much do you agree with each of the following?</p>
Screen 17 of 22

What is your gender?

  • Male
  • Female
  • Prefer not to say
genderMultiple Choice (MC / SAVR)Force Response
Verbatim from the parent studies, including the choice list, so the data pool. If you want a more inclusive item, ADD a separate gender-identity question rather than editing this one — changing it breaks comparability with the Sep 2023 samples.
Records: gender
HTML to paste into the question's HTML View27 bytes
<p>What is your gender?</p>
Screen 18 of 22

What is your age?

years
ageText Entry (TE), Content Type = Number
Verbatim from the parent studies.
Records: age
HTML to paste into the question's HTML View24 bytes
<p>What is your age?</p>
Screen 19 of 22

What is your ethnicity? Select all that apply.

  • American Indian or Alaska Native
  • Asian
  • Black or African American
  • Hispanic or Latino
  • Native Hawaiian or Pacific Islander
  • White
  • Prefer not to say
ethnicityMultiple Choice (MC / MAVR), multi-select
Verbatim from the parent studies, multi-select.
Records: ethnicity
HTML to paste into the question's HTML View53 bytes
<p>What is your ethnicity? Select all that apply.</p>
Screen 20 of 22

What is your highest degree of education?

  • Less than high school
  • High school graduate
  • Some college
  • 2 year college
  • 4 year college
  • Professional degree
  • Doctorate
educationMultiple Choice (MC / SAVR)
Verbatim from the parent studies.
Records: education
HTML to paste into the question's HTML View48 bytes
<p>What is your highest degree of education?</p>
Screen 21 of 22

What was your total household income last year, before taxes?

  • Less than $25,000
  • $25,000 – $49,999
  • $50,000 – $74,999
  • $75,000 – $99,999
  • $100,000 – $149,999
  • $150,000 or more
  • Prefer not to say
incomeMultiple Choice (MC / SAVR)
DELIBERATE CHANGE from the parent studies, which asked for a dollar amount as open text. Open-text income produces unusable answers (“50k”, “idk”, blanks) and high refusal. Brackets cost exact comparability on this one variable — if you would rather keep it poolable, swap back to a Text Entry question.
Records: income
HTML to paste into the question's HTML View68 bytes
<p>What was your total household income last year, before taxes?</p>
Block 10debriefnot shown to participants
Create a Qualtrics block named debrief holding 1 question, in the order below.
Final screen. The Survey Flow ends with an End of Survey element that redirects to the Prolific completion URL.
Screen 22 of 22

Thank you — you are finished.

This study is about how gender-typed everyday thinking tasks are perceived to be: for each task, whether people believe men or women tend to perform better. We are comparing those beliefs against how people actually perform on a few of the same tasks.

The tasks were real and your scores were real; nothing was faked.

Your bonus. On each of the two timed tasks, the top 10% of participants earn a $2 bonus, based on the best of your three attempts, with ties broken by speed where possible — up to $4 in total. We can only rank scores once everyone has finished, so bonuses are paid through Prolific within 7 days of the study closing. You do not need to do anything.

If you have questions about the study, contact Nathan Barrymore at nathan.barrymore@mccombs.utexas.edu.

Please click Next to register your completion and return to Prolific.

debriefDescriptive Text (DB)
DEBRIEF — Descriptive Text (DB). Last screen before the Prolific redirect.
HTML to paste into the question's HTML View968 bytes
<p><strong>Thank you &mdash; you are finished.</strong></p>
<p>This study is about how <em>gender-typed</em> everyday thinking tasks are perceived to be: for each
task, whether people believe men or women tend to perform better. We are comparing those beliefs
against how people actually perform on a few of the same tasks.</p>
<p>The tasks were real and your scores were real; nothing was faked.</p>
<p><strong>Your bonus.</strong> On each of the two timed tasks, the top 10% of participants earn a $2
bonus, based on the best of your three attempts, with ties broken by speed where possible &mdash; up to
$4 in total. We can only rank scores once everyone has finished, so bonuses are paid through Prolific
within 7 days of the study closing. You do not need to do anything.</p>
<p>If you have questions about the study, contact Nathan Barrymore at
nathan.barrymore@mccombs.utexas.edu.</p>
<p>Please click Next to register your completion and return to Prolific.</p>