From 9b226d60da219074a02788d33616caf4092170e0 Mon Sep 17 00:00:00 2001 From: Shaheed Azaad Date: Fri, 20 Feb 2026 00:49:20 +0100 Subject: [PATCH] working prototype --- index.js | 92 ++++++++++- plugins/jspsych-captcha.js | 303 ++++++++++++++++++++++++++++++++++++ plugins/jspsych-die-roll.js | 209 +++++++++++++++++++++++++ scripts/text-stimuli.js | 114 ++++++++++---- styles.css | 127 ++++++++++++++- 5 files changed, 810 insertions(+), 35 deletions(-) create mode 100644 plugins/jspsych-captcha.js create mode 100644 plugins/jspsych-die-roll.js diff --git a/index.js b/index.js index 33bda1e..55d61de 100644 --- a/index.js +++ b/index.js @@ -8,13 +8,17 @@ import '@jspsych/plugin-survey/css/survey.css'; import './styles.css'; import { getStimulusMap } from './scripts/text-stimuli.js'; import { textStimuli } from './scripts/text-stimuli.js'; +import JsPsychDieRoll from './plugins/jspsych-die-roll.js'; +import JsPsychCaptcha from './plugins/jspsych-captcha.js'; +import html from './utils/html.js'; const experiment_name = import.meta.env.VITE_EXPERIMENT_NAME; let prolific_id; let probe_condition; // will be set to ai or human based on the condition. -let debug = false; +let debug; let probe_order; // will be set to ai_first or human_first based on the condition +const CAPTCHA_TRIAL_COUNT = debug ? 2 : 12; // set to 10 for main experiment function delayed_redirect(url) { setTimeout(() => { @@ -34,9 +38,14 @@ const jsPsych = initJsPsych({ console.log(jsPsych.data.get().json()); } }, + show_progress_bar: true, }); -debug = jsPsych.data.getURLVariable('debug') === 'true'; +debug = + jsPsych.data.getURLVariable('debug') === 'true' || + import.meta.env.VITE_DEBUG === 'true'; + +console.log('debug:', debug); prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID'); @@ -54,22 +63,28 @@ const probe_text_difficulty = 'while the die roll was random, the difficulty of the captcha task was pre-determined and unrelated to the die roll. ' + probe_closing_text; +let die_result = 3; + switch (COND) { case 0: probe_condition = 'die'; probe_order = 'die_first'; + die_result = 4; break; case 1: probe_condition = 'die'; probe_order = 'die_first'; + die_result = 4; break; case 2: probe_condition = 'difficulty'; probe_order = 'die_first'; + die_result = 4; break; case 3: probe_condition = 'difficulty'; probe_order = 'die_first'; + die_result = 4; break; case 4: probe_condition = 'die'; @@ -135,6 +150,18 @@ const instructions_1 = { stimulus: stimulusMap.get('instructions_1'), }; +const instructions_2 = { + type: jsPsychHtmlKeyboardResponse, + choices: [' '], + stimulus: stimulusMap.get('instructions_2'), +}; + +const instructions_3 = { + type: jsPsychHtmlKeyboardResponse, + choices: [' '], + stimulus: stimulusMap.get('instructions_3'), +}; + const debrief = { type: jsPsychHtmlKeyboardResponse, choices: [' '], @@ -147,6 +174,40 @@ const pre_survey_info = { stimulus: stimulusMap.get('pre_survey_info'), }; +const die_roll_trial = { + type: JsPsychDieRoll, + prompt: html` +

+ Click the die once to start it rolling, then click it again to stop. +

+ `, + rigged_value: die_result, + roll_duration: 2200, + result_template: `You rolled a {{value}} (moderate difficulty). Click 'continue' to proceed.`, +}; + +const create_captcha_trial = trial_index => { + return { + type: JsPsychCaptcha, + prompt: + '

Enter the characters you see into the textbox below.

', + difficulty: die_result, + button_label: 'Continue', + error_text: '', + allow_refresh: false, + require_correct: false, + data: { + trial_id: 'captcha_entry', + captcha_index: trial_index + 1, + captcha_total: CAPTCHA_TRIAL_COUNT, + }, + }; +}; + +const captcha_trials = Array.from({ length: CAPTCHA_TRIAL_COUNT }, (_, index) => + create_captcha_trial(index) +); + const die_probe_row = { text: 'I suspected that the die roll was not random, or the number I received was pre-determined.', @@ -209,6 +270,14 @@ const survey = { text: `I found the captcha task difficult.`, value: 'Difficulty', }, + { + text: `I felt lucky during the die roll.`, + value: 'Luck', + }, + { + text: `I felt unlucky during the die roll.`, + value: 'Bad_luck', + }, { text: `I think I solved all the captchas correctly.`, value: 'Accuracy', @@ -331,18 +400,25 @@ const survey = { }, }; +const main_experiment_timeline = [ + instructions_1, + instructions_2, + die_roll_trial, + instructions_3, + ...captcha_trials, + pre_survey_info, + survey, + debrief, +]; + if (debug) { - timeline.push(survey); - timeline.push(debrief); + timeline.push(...main_experiment_timeline); } if (!debug) { timeline.push(pre_consent_info); timeline.push(consent_form); timeline.push(enter_fullscreen); - timeline.push(instructions_1); - timeline.push(pre_survey_info); - timeline.push(survey); - timeline.push(debrief); + timeline.push(...main_experiment_timeline); } jsPsych.run(timeline); diff --git a/plugins/jspsych-captcha.js b/plugins/jspsych-captcha.js new file mode 100644 index 0000000..9d1a25d --- /dev/null +++ b/plugins/jspsych-captcha.js @@ -0,0 +1,303 @@ +import { ParameterType } from 'jspsych'; + +const info = { + name: 'captcha', + parameters: { + prompt: { + type: ParameterType.HTML_STRING, + default: '', + }, + difficulty: { + type: ParameterType.INT, + default: 3, + }, + button_label: { + type: ParameterType.STRING, + default: 'Submit', + }, + allow_refresh: { + type: ParameterType.BOOL, + default: true, + }, + require_correct: { + type: ParameterType.BOOL, + default: true, + }, + error_text: { + type: ParameterType.HTML_STRING, + default: 'That entry did not match, please try again.', + }, + }, +}; + +class CaptchaPlugin { + constructor(jsPsych) { + this.jsPsych = jsPsych; + this.characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + } + + trial(display_element, trial) { + const start_time = performance.now(); + const attempt_counts = { submission_count: 0, refresh_count: 0 }; + const difficulty_settings = this.resolve_difficulty(trial.difficulty); + let captcha_text = this.generate_text(difficulty_settings.length); + let last_target_at_submission = captcha_text; + + display_element.innerHTML = this.build_markup( + trial.prompt, + trial.button_label, + trial.allow_refresh + ); + + const canvas_element = display_element.querySelector( + '.jspsych_captcha_canvas' + ); + const canvas_context = canvas_element.getContext('2d'); + const input_element = display_element.querySelector( + '.jspsych_captcha_input' + ); + const button_element = display_element.querySelector( + '.jspsych_captcha_submit' + ); + const refresh_button = display_element.querySelector( + '.jspsych_captcha_refresh' + ); + const error_element = display_element.querySelector( + '.jspsych_captcha_error' + ); + + const draw_captcha = () => { + this.draw_captcha(canvas_context, captcha_text, difficulty_settings); + }; + + draw_captcha(); + input_element.focus(); + + if (refresh_button) { + refresh_button.addEventListener('click', () => { + attempt_counts.refresh_count += 1; + captcha_text = this.generate_text(difficulty_settings.length); + draw_captcha(); + error_element.textContent = ''; + input_element.value = ''; + input_element.focus(); + }); + } + + const length_hint_element = display_element.querySelector( + '.jspsych_captcha_length_hint' + ); + + const update_button_state = () => { + const current_length = input_element.value.trim().length; + const required_length = captcha_text.length; + button_element.disabled = current_length < required_length; + if (length_hint_element) { + length_hint_element.textContent = + current_length < required_length + ? `Type ${required_length - current_length} more character(s) to continue.` + : ''; + } + }; + + const finish_trial = (response_value, is_correct) => { + const trial_data = { + rt: performance.now() - start_time, + response: response_value, + participant_entry: response_value, + participant_entry_length: response_value.length, + captcha_target: last_target_at_submission, + correct: is_correct, + submission_count: attempt_counts.submission_count, + manual_refreshes: attempt_counts.refresh_count, + difficulty_label: difficulty_settings.label, + difficulty_length: difficulty_settings.length, + difficulty_level: difficulty_settings.level, + }; + display_element.innerHTML = ''; + this.jsPsych.finishTrial(trial_data); + }; + + const validate_response = () => { + attempt_counts.submission_count += 1; + const response_value = input_element.value.trim().toUpperCase(); + last_target_at_submission = captcha_text; + const is_correct = response_value === captcha_text; + + if (response_value.length < captcha_text.length) { + error_element.textContent = `Please enter all ${captcha_text.length} characters shown in the captcha.`; + update_button_state(); + return; + } + + if (is_correct || !trial.require_correct) { + finish_trial(response_value, is_correct); + return; + } + + error_element.textContent = trial.error_text; + captcha_text = this.generate_text(difficulty_settings.length); + draw_captcha(); + input_element.value = ''; + input_element.focus(); + }; + + button_element.addEventListener('click', validate_response); + input_element.addEventListener('keydown', event => { + if (event.key === 'Enter') { + event.preventDefault(); + validate_response(); + } + }); + input_element.addEventListener('input', update_button_state); + update_button_state(); + } + + build_markup(prompt, button_label, allow_refresh) { + const prompt_html = prompt + ? `
${prompt}
` + : ''; + const refresh_button = allow_refresh + ? '' + : ''; + + return ` +
+ ${prompt_html} +
+ + ${refresh_button} +
+ + +
+
+ `; + } + + draw_captcha(canvas_context, captcha_text, settings) { + const { width, height } = canvas_context.canvas; + canvas_context.clearRect(0, 0, width, height); + const gradient = canvas_context.createLinearGradient(0, 0, width, height); + gradient.addColorStop(0, '#f8fafc'); + gradient.addColorStop(1, '#e2e8f0'); + canvas_context.fillStyle = gradient; + canvas_context.fillRect(0, 0, width, height); + + for (let index = 0; index < settings.noise_lines; index++) { + canvas_context.beginPath(); + canvas_context.moveTo(Math.random() * width, Math.random() * height); + canvas_context.bezierCurveTo( + Math.random() * width, + Math.random() * height, + Math.random() * width, + Math.random() * height, + Math.random() * width, + Math.random() * height + ); + canvas_context.lineWidth = 1 + Math.random() * 2; + canvas_context.strokeStyle = `rgba(30, 41, 59, ${0.15 + + Math.random() * 0.2})`; + canvas_context.stroke(); + } + + canvas_context.font = `bold ${settings.font_size}px 'Courier New', monospace`; + canvas_context.textBaseline = 'middle'; + canvas_context.textAlign = 'center'; + + const horizontal_step = width / (captcha_text.length + 1); + const center_y = height / 2; + + for (let index = 0; index < captcha_text.length; index++) { + const current_character = captcha_text[index]; + canvas_context.save(); + const jitter_y = this.random_between(-settings.jitter, settings.jitter); + canvas_context.translate( + horizontal_step * (index + 1), + center_y + jitter_y + ); + canvas_context.rotate( + this.random_between(-settings.rotation, settings.rotation) + ); + canvas_context.fillStyle = '#000000'; + canvas_context.fillText(current_character, 0, 0); + canvas_context.restore(); + } + + const noise_pixels = Math.floor(width * height * settings.speckle_density); + const image_data = canvas_context.getImageData(0, 0, width, height); + for (let index = 0; index < noise_pixels; index++) { + const pixel_offset = + (Math.floor(Math.random() * width) + + Math.floor(Math.random() * height) * width) * + 4; + const shade = Math.random() > 0.5 ? 255 : 0; + image_data.data[pixel_offset] = shade; + image_data.data[pixel_offset + 1] = shade; + image_data.data[pixel_offset + 2] = shade; + image_data.data[pixel_offset + 3] = 255; + } + canvas_context.putImageData(image_data, 0, 0); + } + + generate_text(length) { + let generated_text = ''; + for (let index = 0; index < length; index++) { + generated_text += this.characters.charAt( + Math.floor(Math.random() * this.characters.length) + ); + } + return generated_text; + } + + resolve_difficulty(difficulty_input) { + const default_level = 3; + let normalized_level = Number(difficulty_input); + if (!Number.isFinite(normalized_level)) { + normalized_level = default_level; + } + normalized_level = Math.round(Math.min(6, Math.max(1, normalized_level))); + + const base_length = 7; + const base_font_size = 20; + const base_rotation = 0.25; + const max_rotation = base_rotation * 3; + const base_noise_lines = 4; + const base_speckle_density = 0.01; + const base_jitter = 25; + const noise_line_step = 0.8; + const speckle_step = 0.0015; + const jitter_step = 1.6; + const rotation_step = (max_rotation - base_rotation) / 5; + + const rotation = base_rotation + rotation_step * (normalized_level - 1); + const noise_lines = Math.round( + base_noise_lines + noise_line_step * (normalized_level - 1) + ); + const speckle_density = + base_speckle_density + speckle_step * (normalized_level - 1); + const jitter = base_jitter + jitter_step * (normalized_level - 1); + + return { + length: base_length, + noise_lines: noise_lines, + rotation: rotation, + jitter: jitter, + font_size: base_font_size, + speckle_density: speckle_density, + label: `level_${normalized_level}`, + level: normalized_level, + }; + } + + random_between(min, max) { + return Math.random() * (max - min) + min; + } +} + +CaptchaPlugin.info = info; + +export default CaptchaPlugin; diff --git a/plugins/jspsych-die-roll.js b/plugins/jspsych-die-roll.js new file mode 100644 index 0000000..d6202fa --- /dev/null +++ b/plugins/jspsych-die-roll.js @@ -0,0 +1,209 @@ +import { ParameterType } from 'jspsych'; + +const info = { + name: 'die-roll', + parameters: { + prompt: { + type: ParameterType.HTML_STRING, + default: '', + }, + rigged_value: { + type: ParameterType.INT, + default: null, + }, + roll_duration: { + type: ParameterType.INT, + default: 2000, + }, + animation_interval: { + type: ParameterType.INT, + default: 120, + }, + button_label: { + type: ParameterType.STRING, + default: 'Continue', + }, + result_template: { + type: ParameterType.STRING, + default: 'You rolled a {{value}}.', + }, + }, +}; + +class DieRollPlugin { + constructor(jsPsych) { + this.jsPsych = jsPsych; + } + + trial(display_element, trial) { + const start_time = performance.now(); + const sanitized_rigged_value = this.get_rigged_value(trial.rigged_value); + const final_value = + sanitized_rigged_value ?? + this.jsPsych.randomization.sampleWithoutReplacement( + [1, 2, 3, 4, 5, 6], + 1 + )[0]; + + display_element.innerHTML = this.build_markup( + trial.prompt, + trial.button_label + ); + + const face_element = display_element.querySelector( + '.jspsych_die_roll_face' + ); + const result_element = display_element.querySelector( + '.jspsych_die_roll_result' + ); + const button_element = display_element.querySelector( + '.jspsych_die_roll_button' + ); + + let is_rolling = false; + let has_rolled = false; + let roll_started_at = null; + let roll_stopped_at = null; + let die_click_count = 0; + + const clear_timeouts = () => { + this.jsPsych.pluginAPI.clearAllTimeouts(); + }; + + const cycle_face = () => { + if (!is_rolling) { + return; + } + const value = this.random_face(final_value); + face_element.textContent = value; + this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval); + }; + + const finalize_roll = () => { + if (!is_rolling || has_rolled) { + return; + } + is_rolling = false; + has_rolled = true; + die_click_count += 1; + roll_stopped_at = performance.now(); + clear_timeouts(); + face_element.textContent = final_value; + face_element.classList.remove('jspsych_die_roll_face--active'); + face_element.classList.remove('jspsych_die_roll_face--interactive'); + face_element.classList.add('jspsych_die_roll_face--locked'); + face_element.setAttribute('aria-disabled', 'true'); + face_element.removeAttribute('tabindex'); + face_element.style.pointerEvents = 'none'; + result_element.textContent = trial.result_template.replace( + /\{\{value\}\}/gi, + final_value + ); + button_element.disabled = false; + button_element.focus(); + }; + + const begin_roll = () => { + if (is_rolling || has_rolled) { + return; + } + is_rolling = true; + die_click_count += 1; + roll_started_at = performance.now(); + result_element.textContent = 'Rolling... click again to stop the die.'; + face_element.classList.add('jspsych_die_roll_face--active'); + button_element.disabled = true; + clear_timeouts(); + cycle_face(); + }; + + const handle_face_interaction = () => { + if (!is_rolling && !has_rolled) { + begin_roll(); + } else if (is_rolling && !has_rolled) { + finalize_roll(); + } + }; + + button_element.disabled = true; + face_element.textContent = ''; + result_element.textContent = + 'Click the die to start, then click it again to lock in your number.'; + face_element.classList.add('jspsych_die_roll_face--interactive'); + face_element.setAttribute('tabindex', '0'); + face_element.setAttribute('role', 'button'); + face_element.setAttribute( + 'aria-label', + 'Virtual die. Click to start and stop.' + ); + + face_element.addEventListener('click', handle_face_interaction); + face_element.addEventListener('keydown', event => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handle_face_interaction(); + } + }); + + const end_trial = () => { + is_rolling = false; + clear_timeouts(); + const trial_data = { + roll: final_value, + rigged_value: sanitized_rigged_value, + rigged: sanitized_rigged_value !== null, + die_click_count: die_click_count, + roll_started_after_ms: roll_started_at + ? roll_started_at - start_time + : null, + roll_duration_ms: + roll_started_at && roll_stopped_at + ? roll_stopped_at - roll_started_at + : null, + rt: performance.now() - start_time, + }; + display_element.innerHTML = ''; + this.jsPsych.finishTrial(trial_data); + }; + + button_element.addEventListener('click', () => { + if (!button_element.disabled) { + end_trial(); + } + }); + } + + build_markup(prompt, button_label) { + const prompt_html = prompt + ? `
${prompt}
` + : ''; + return ` +
+ ${prompt_html} +
?
+
+ +
+ `; + } + + random_face(exclude_value) { + let value = Math.floor(Math.random() * 6) + 1; + if (value === exclude_value) { + value = (value % 6) + 1; + } + return value; + } + + get_rigged_value(value) { + const numeric = Number(value); + if (Number.isFinite(numeric) && numeric >= 1 && numeric <= 6) { + return Math.round(numeric); + } + return null; + } +} + +DieRollPlugin.info = info; + +export default DieRollPlugin; diff --git a/scripts/text-stimuli.js b/scripts/text-stimuli.js index f8ef0e4..22f3b26 100644 --- a/scripts/text-stimuli.js +++ b/scripts/text-stimuli.js @@ -9,26 +9,30 @@ export const textStimuli = { }; function getStimulusMap() { - const stimulusMap = new Map(); - - stimulusMap.set('pre_survey_info', html` -

- You will now answer a few questions about your experience in the experiment. It is important that you read the questions carefully and answer them honestly. -

-

- Nonsense or random answers may lead to your submission being rejected. -

-

- Press - SPACE - to continue. -

- ` + stimulusMap.set( + 'pre_survey_info', + html` +

+ You will now answer a few questions about your experience in the + experiment. It is important that you read the questions carefully and + answer them honestly. +

+

+ Nonsense or random answers may lead to your submission being rejected. +

+

+ Press + SPACE + to continue. +

+ ` ); - stimulusMap.set( 'pre_consent_info', html` @@ -105,15 +109,16 @@ function getStimulusMap() {

The controller within the meaning of the EU General Data Protection Regulation (GDPR) and other national data protection laws of the member - states, as well as other data protection regulations is the University of Muenster, represented by the Rector, Prof. Dr. - Johannes Wessels, Schlossplatz 2, 48149 MünsterTel.: + 49 251 - 83-0E-Mail: verwaltung@uni-muenster.de + states, as well as other data protection regulations is the University + of Muenster, represented by the Rector, Prof. Dr. Johannes Wessels, + Schlossplatz 2, 48149 MünsterTel.: + 49 251 83-0E-Mail: + verwaltung@uni-muenster.de

8. Contact details of the data protection officer

- The data protection officer of the University of Muenster is: Nina Meyer-Pachur - Schlossplatz 2, 48149 Münster Tel.: + 49 251 83-22446 E-Mail: - datenschutz@uni-muenster.de + The data protection officer of the University of Muenster is: Nina + Meyer-Pachur Schlossplatz 2, 48149 Münster Tel.: + 49 251 83-22446 + E-Mail: datenschutz@uni-muenster.de

9. Reference to the rights of those affected

@@ -159,14 +164,68 @@ function getStimulusMap() { ` ); - stimulusMap.set( 'instructions_1', html` - TEST +

In this experiment, you will be solving some captchas.

+

+ This will involve identifying slightly distorted letters and numbers + from an image. +

+

+ Your task is to solve the captchas as quickly and accurately as + possible. You will not be given feedback on your performance. +

+

+ Press + SPACE + to continue. +

` ); + stimulusMap.set( + 'instructions_2', + html` +

+ To randomise the difficulty of the captchas you will be solving, we will + first ask you to roll a virtual die. You will then be shown captchas of + a difficulty corresponding to the number you rolled. +

+

+ The higher the number you roll, the more difficult the captchas will be. +

+

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'instructions_3', + html` +

+ You will now proceed to the captcha task, in which you will need to + solve 12 captchas in a row as quickly and accurately as possible. +

+

+ Please ensure that you are in a quiet environment and that you will not + be interrupted for the next few minutes. +

+

+ Please also ensure that you solve these captchas to the best of your + ability. Nonsense or random answers may lead to your submission being + rejected. +

+

+ Press + SPACE + when you are ready to begin. +

+ ` + ); stimulusMap.set( 'debrief', @@ -184,10 +243,13 @@ function getStimulusMap() {

To this end, we created the appearance of additional participants in - the experiment – in reality, you performed the task alone with your 'partner's' actions being peformed by a computer. + the experiment – in reality, you performed the task alone with your + 'partner's' actions being peformed by a computer.

- You may have also read in the post-experiment questionnaire that your partner was either an AI agent – this was to examine whether people’s suspicions are biased by the way in which they are asked about them. + You may have also read in the post-experiment questionnaire that your + partner was either an AI agent – this was to examine whether people’s + suspicions are biased by the way in which they are asked about them.

Should you have any additional questions, you may contact Dr Shaheed diff --git a/styles.css b/styles.css index 1554e7f..bbc53c7 100644 --- a/styles.css +++ b/styles.css @@ -29,4 +29,129 @@ .object-moving-box { width: 3vw; height: 3vw; -} \ No newline at end of file +} + +.jspsych_die_roll { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 1.25rem; +} + +.jspsych_die_roll_prompt { + max-width: 40rem; +} + +.jspsych_die_roll_face { + width: 7.5rem; + height: 7.5rem; + border-radius: 1rem; + border: 4px solid #0f172a; + display: flex; + align-items: center; + justify-content: center; + font-size: 3.5rem; + font-weight: 700; + background: linear-gradient(145deg, #f1f5f9, #e2e8f0); + box-shadow: 0 20px 35px rgba(15, 23, 42, 0.2); + cursor: pointer; + user-select: none; + transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease; +} + +.jspsych_die_roll_result { + font-weight: 600; + min-height: 1.5rem; +} + +.jspsych_die_roll_face--interactive:hover { + transform: scale(1.03); + box-shadow: 0 24px 40px rgba(15, 23, 42, 0.25); +} + +.jspsych_die_roll_face--active { + animation: pulse 0.6s ease-in-out infinite alternate; +} + +.jspsych_die_roll_face--locked { + cursor: not-allowed; + opacity: 0.8; + box-shadow: 0 15px 25px rgba(15, 23, 42, 0.15); +} + +@keyframes pulse { + from { + transform: scale(1); + } + to { + transform: scale(1.05); + } +} + +.jspsych_captcha { + max-width: 34rem; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + text-align: center; +} + +.jspsych_captcha_canvas_wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + width: 100%; +} + +.jspsych_captcha_canvas { + border-radius: 0.75rem; + border: 2px solid #cbd5f5; + background: #fff; +} + +.jspsych_captcha_refresh { + border: none; + background: #e2e8f0; + border-radius: 9999px; + width: 2.75rem; + height: 2.75rem; + font-size: 1.35rem; + cursor: pointer; +} + +.jspsych_captcha_label { + display: flex; + flex-direction: column; + gap: 0.4rem; + font-weight: 600; + align-items: center; + width: 100%; +} + +.jspsych_captcha_input { + border: 2px solid #cbd5f5; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + font-size: 1.25rem; + letter-spacing: 0.2rem; + text-transform: uppercase; + text-align: center; + max-width: 16rem; +} + +.jspsych_captcha_length_hint { + font-size: 0.9rem; + font-weight: 500; + color: #475569; + min-height: 1.25rem; +} + +.jspsych_captcha_error { + min-height: 1.25rem; + color: #b91c1c; + font-weight: 600; +}