Relume Style Guide

Form-Validation
Form Styles
form_field-label
form_input
form_input
is-text-area
form_input
is-select-input
form_checkbox
form_radio
form-field
is-button

Submit

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
To use Forms on a dark background, simply use the add-on class is-alternate
form_field-label
form_input
form_input
is-text-area
form_input
is-select-input
form_checkbox
form_radio
form-field
is-button

Submit

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Form to Copy

Submit

Success! We’ll be in touch soon.
Something went wrong while submitting.
How to use documentation
# Form Validation System Documentation

## Overview

This form validation system provides real-time validation with visual feedback for various input types. It supports text inputs, emails, phone numbers, textareas, select dropdowns, radio buttons, and checkboxes.

## Quick Start

### Basic Structure
```html
<div data-form-validate="" class="form-group w-form">
  <form>
    <div data-validate="" class="form-field-group">
      <!-- Your input fields here -->
    </div>
  </form>
</div>
```

**Required Attributes:**
- `data-form-validate` - Container for the entire form
- `data-validate` - Each field group that needs validation
- `data-submit` - Submit button wrapper

---

## Field Types & Validation Rules

### 1. Text Input (Name, Generic Text)

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <label for="name" class="form-label">Name <span class="form-required">*</span></label>
  <div class="form-field">
    <input class="form-input w-input" 
           type="text" 
           id="name" 
           name="name"
           min="1"
           maxlength="256"
           required="">
  </div>
</div>
```

**Validation Rules:**
- **Minimum length**: Set via `min` attribute (e.g., `min="1"`)
- **Maximum length**: Set via `maxlength` attribute (e.g., `maxlength="256"`)
- **Required**: Add `required` attribute

**Behavior:**
- Validation starts when user types beyond min length
- Shows error if length is below min or above max

---

### 2. Email Input

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <label for="email" class="form-label">Email <span class="form-required">*</span></label>
  <div class="form-field">
    <input class="form-input w-input" 
           type="email" 
           id="email" 
           name="email"
           maxlength="256"
           required="">
  </div>
</div>
```

**Validation Rules:**
- **Pattern**: Must match `[text]@[domain].[extension]`
- **Regex**: `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`
- **Required**: Add `required` attribute

**Valid Examples:**
- `hello@osmo.supply`
- `user.name+tag@example.co.uk`

**Invalid Examples:**
- `user@domain` (missing TLD)
- `@domain.com` (missing local part)
- `user domain@test.com` (contains spaces)

**Behavior:**
- Validation starts automatically when valid email format is detected
- Also triggers on blur (when field loses focus)

---

### 3. Phone Input (Tel)

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <label for="phone" class="form-label">Phone <span class="form-required">*</span></label>
  <div class="form-field">
    <input class="form-input w-input" 
           type="tel" 
           id="phone" 
           name="phone"
           placeholder="+1 234 567 8900"
           required="">
  </div>
</div>
```

**Validation Rules:**
- **Pattern**: `/^(\+?\d{7,15})$/` (after cleaning)
- **Minimum**: 7 digits (local short numbers)
- **Maximum**: 15 digits (international standard)
- **Optional +**: For international format
- **Character Filtering**: Users CANNOT type letters - only numbers and phone characters

**Allowed Input Characters:**
- Numbers: `0-9`
- Formatting: `+`, `-`, `(`, `)`, space
- Navigation keys and copy/paste shortcuts

**Valid Examples:**
- Local: `555-1234`, `(555) 123-4567`, `555.123.4567`
- International: `+1 555 123 4567`, `+44 20 1234 5678`, `+31 20 123 4567`

**Special Features:**
- **Letter Blocking**: Users cannot type letters (a-z) at all
- **Auto-Clean Paste**: If invalid characters are pasted, they're automatically removed
- **Format Flexibility**: All formatting (spaces, dashes, parentheses) is stripped before validation

**Behavior:**
- Validation starts automatically when valid phone format is detected
- Also triggers on blur

---

### 4. Textarea (Message, Long Text)

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <label for="message" class="form-label">Message <span class="form-required">*</span></label>
  <div class="form-field">
    <textarea class="form-input is--textarea w-input" 
              id="message" 
              name="message"
              min="3"
              maxlength="5000"
              placeholder="Your message..."
              required=""></textarea>
  </div>
</div>
```

**Validation Rules:**
- **Minimum length**: Set via `min` attribute (e.g., `min="3"`)
- **Maximum length**: Set via `maxlength` attribute (e.g., `maxlength="5000"`)
- **Required**: Add `required` attribute

**Behavior:**
- Same as text input
- Allows Enter key for new lines (doesn't submit form)

---

### 5. Select Dropdown

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <label for="category" class="form-label">Category <span class="form-required">*</span></label>
  <div class="form-field">
    <select id="category" 
            name="category" 
            class="form-input w-select" 
            required="">
      <option value="">Select option</option>
      <option value="First">First choice</option>
      <option value="Second">Second choice</option>
      <option value="Third">Third choice</option>
    </select>
  </div>
</div>
```

**Validation Rules:**
- Must have a valid value selected
- **Invalid values** (auto-disabled on page load):
  - Empty string: `value=""`
  - `value="disabled"`
  - `value="null"`
  - `value="false"`

**Behavior:**
- Validation starts immediately on selection change
- First option (placeholder) should have `value=""` to force selection

---

### 6. Radio Buttons

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <label class="form-label">Mode <span class="form-required">*</span></label>
  <div data-radiocheck-group="" class="radiocheck-group">
    <label class="radiocheck-field w-radio">
      <input type="radio" name="Mode" id="dark-mode" value="Dark mode" class="w-form-formradioinput radio-input w-radio-input">
      <span class="radiocheck-label w-form-label">Dark mode</span>
    </label>
    <label class="radiocheck-field w-radio">
      <input type="radio" name="Mode" id="light-mode" value="Light mode" class="w-form-formradioinput radio-input w-radio-input">
      <span class="radiocheck-label w-form-label">Light mode</span>
    </label>
  </div>
</div>
```

**Required Attribute:**
- `data-radiocheck-group` on the container

**Validation Rules:**
- **Required (default)**: At least 1 option must be selected
- **Optional**: Add `min="0"` to make optional

**Attributes:**
- `min` - Minimum selections required (default: 1)
  - `min="0"` - Optional (no selection required)
  - `min="1"` - Required (at least 1 selection)
  - No min attribute - Defaults to required (min="1")

**Behavior:**
- Validation starts when minimum requirement is met
- Also triggers on blur

---

### 7. Checkbox Group (Multiple Selection)

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <label class="form-label">Location <span class="form-required">*</span> 
    <span class="form-inactive-text">(select min. 2)</span>
  </label>
  <div data-radiocheck-group="" min="2" max="2" class="radiocheck-group">
    <label class="w-checkbox radiocheck-field">
      <input type="checkbox" name="Location-Netherlands" id="Location-Netherlands" class="w-checkbox-input checkbox-input">
      <span class="radiocheck-label w-form-label">The Netherlands</span>
    </label>
    <label class="w-checkbox radiocheck-field">
      <input type="checkbox" name="Location-Germany" id="Location-Germany" class="w-checkbox-input checkbox-input">
      <span class="radiocheck-label w-form-label">Germany</span>
    </label>
    <label class="w-checkbox radiocheck-field">
      <input type="checkbox" name="Location-Belgium" id="Location-Belgium" class="w-checkbox-input checkbox-input">
      <span class="radiocheck-label w-form-label">Belgium</span>
    </label>
  </div>
</div>
```

**Required Attribute:**
- `data-radiocheck-group` on the container

**Validation Rules:**
- **Minimum**: Set via `min` attribute (default: 1)
- **Maximum**: Set via `max` attribute (default: number of checkboxes)

**Attributes:**
- `min="2"` - At least 2 selections required
- `max="2"` - Maximum 2 selections allowed
- `min="0"` - Optional (no selection required)
- No attributes - At least 1 required, all can be selected

**Common Patterns:**
- **Exact number**: `min="2" max="2"` - Must select exactly 2
- **At least**: `min="2"` - Select 2 or more
- **Optional multiple**: `min="0" max="3"` - Select up to 3, or none
- **Required single**: No attributes - At least 1 must be selected

**Behavior:**
- Validation starts when minimum requirement is met
- Also triggers on blur

---

### 8. Single Checkbox (Terms & Conditions)

**HTML Example:**
```html
<div data-validate="" class="form-field-group">
  <div data-radiocheck-group="" class="radiocheck-group">
    <label class="w-checkbox radiocheck-field">
      <input type="checkbox" 
             name="Terms-Conditions" 
             id="Terms-Conditions" 
             required="" 
             class="w-checkbox-input checkbox-input">
      <span class="radiocheck-label w-form-label">
        Accept Terms & Conditions <span class="form-required">*</span>
      </span>
    </label>
  </div>
</div>
```

**Validation Rules:**
- Must be checked to pass validation
- Automatically detected when there's only 1 checkbox in the group

**Behavior:**
- Validation starts immediately on check/uncheck
- Also triggers on blur

---

## Visual States & CSS Classes

### Dynamic State Classes

The system automatically adds/removes these classes based on field state:

#### 1. `is--filled`
- **Added when**: Field has content or option is selected
- **Removed when**: Field is empty
- **Use for**: Floating labels, placeholder animations, styling filled states

```css
.form-field-group.is--filled .form-label {
  /* Style for filled state */
}
```

#### 2. `is--success`
- **Added when**: Field passes validation
- **Removed when**: Field becomes invalid
- **Use for**: Success icons, green borders, positive feedback

```css
.form-field-group.is--success .form-input {
  border-color: green;
}
.form-field-group.is--success .form-field-icon.is--success {
  display: block;
}
```

#### 3. `is--error`
- **Added when**: Field fails validation AND user has interacted with it
- **Removed when**: Field becomes valid
- **Use for**: Error icons, red borders, error messages

```css
.form-field-group.is--error .form-input {
  border-color: red;
}
.form-field-group.is--error .form-field-icon.is--error {
  display: block;
}
```

### State Combinations

| Field State | `is--filled` | `is--success` | `is--error` |
|-------------|--------------|---------------|-------------|
| Empty, untouched | ❌ | ❌ | ❌ |
| Empty, touched | ❌ | ❌ | ✅ |
| Invalid, untouched | ✅ | ❌ | ❌ |
| Invalid, touched | ✅ | ❌ | ✅ |
| Valid | ✅ | ✅ | ❌ |

---

## Validation Triggers

### When Validation Starts

Different field types have different validation triggers:

1. **Text/Textarea Inputs**
   - When user types beyond `min` length
   - When user meets `max` length
   - On blur (field loses focus)

2. **Email/Phone Inputs**
   - When valid format is detected
   - On blur

3. **Select Dropdowns**
   - Immediately on selection change

4. **Radio/Checkbox Groups**
   - When minimum selection count is met
   - On blur

5. **Form Submit**
   - All fields are validated on submit attempt
   - First invalid field receives focus

---

## Special Features

### 1. Spam Protection
- Form cannot be submitted within 5 seconds of page load
- Prevents bot submissions
- Shows alert: "Form submitted too quickly. Please try again."

```javascript
// Built-in, no configuration needed
// Timer starts on page load
```

### 2. Enter Key Handling
- Submits form on Enter key press (except in textareas)
- Validates before submission
- Prevents default form submission behavior

### 3. Disabled Options Auto-Detection
- Select options with invalid values are automatically disabled on page load
- Invalid values: `""`, `"disabled"`, `"null"`, `"false"`

### 4. Focus Management
- On submit attempt, first invalid field receives focus automatically
- Improves UX and accessibility

### 5. Phone Input Character Filtering
- **Prevents typing letters** - users cannot type a-z at all
- Allows: numbers, `+`, `-`, `(`, `)`, space
- Auto-cleans pasted content
- Preserves cursor position during cleaning

---

## Form Submission

### Submit Button Structure
```html
<div data-submit="" class="form-submit-btn">
  <p class="form-submit-btn-p">Submit</p>
  <input type="submit" class="form-submit w-button" value="Submit">
</div>
```

**Required:**
- `data-submit` attribute on wrapper
- Actual `<input type="submit">` inside

### Submission Flow
1. User clicks submit or presses Enter
2. All fields are validated
3. If invalid:
   - Error states shown on all invalid fields
   - First invalid field receives focus
   - Submission prevented
4. If valid:
   - Spam check performed (5 second minimum)
   - Form submits

---

## Success/Error Messages

### Success Message
```html
<div class="form-notifcation w-form-done">
  <div class="form-notification-p">Success! We'll be in touch soon.</div>
</div>
```

### Error Message
```html
<div class="form-notifcation is--error w-form-fail">
  <div class="form-notification-p">Something went wrong while submitting.</div>
</div>
```

**Note:** These are handled by Webflow form submission. Custom handling may be needed for AJAX submissions.

---

## Initialization

The validation system auto-initializes on DOM ready:

```javascript
document.addEventListener('DOMContentLoaded', () => {
  initAdvancedFormValidation();
});
```

**No manual initialization required** - just ensure proper HTML structure with data attributes.

---

## Attribute Reference

### Field Group Attributes
| Attribute | Required | Purpose |
|-----------|----------|---------|
| `data-form-validate` | Yes | Marks form container |
| `data-validate` | Yes | Marks field group for validation |
| `data-radiocheck-group` | For radio/checkbox | Groups radio/checkbox inputs |
| `data-submit` | Yes | Marks submit button wrapper |

### Input Attributes
| Attribute | Applies To | Purpose |
|-----------|------------|---------|
| `required` | All inputs | Field is required |
| `min` | Text, textarea, radio/checkbox groups | Minimum length or selections |
| `max` | Checkbox groups | Maximum selections |
| `maxlength` | Text, email, textarea | Maximum character length |
| `type` | All inputs | Input type (text, email, tel, etc.) |

### Special Values
| Attribute | Value | Effect |
|-----------|-------|--------|
| `min` | `"0"` | Makes radio/checkbox optional |
| `min` | `"1"` or absent | Makes radio/checkbox required |
| `min` | `"2"` | Requires at least 2 selections |
| `max` | `"2"` | Allows maximum 2 selections |

---

## Common Patterns

### Required Text Field with Min/Max Length
```html
<input type="text" name="username" min="3" maxlength="20" required="">
```
- Required
- Minimum 3 characters
- Maximum 20 characters

### Optional Radio Group
```html
<div data-radiocheck-group="" min="0">
  <!-- radio buttons -->
</div>
```
- User can skip this field

### Exact Selection Count
```html
<div data-radiocheck-group="" min="2" max="2">
  <!-- checkboxes -->
</div>
```
- Must select exactly 2 options

### International Phone
```html
<input type="tel" name="phone" placeholder="+1 234 567 8900">
```
- Accepts 7-15 digits
- Optional + for country code
- Letters blocked automatically

---

## Best Practices

1. **Always include labels** - Improves accessibility and UX
2. **Use placeholder text** - Show format examples (especially for phone/email)
3. **Mark required fields** - Use `<span class="form-required">*</span>`
4. **Provide hints** - Use `<span class="form-inactive-text">` for instructions
5. **Test keyboard navigation** - Ensure Tab order is logical
6. **Test with screen readers** - Use proper label associations
7. **Set realistic limits** - Phone: 7-15 digits, names: 1-256 chars
8. **Test paste behavior** - Especially for phone fields
9. **Provide clear error states** - Ensure success/error icons are visible
10. **Test on mobile** - Keyboard types should match input types

---

## Troubleshooting

### Validation Not Working
- ✅ Check `data-form-validate` on form container
- ✅ Check `data-validate` on field group
- ✅ Check `data-submit` on submit button wrapper
- ✅ Ensure JavaScript is loaded after DOM

### Radio/Checkbox Not Validating
- ✅ Check `data-radiocheck-group` on container
- ✅ Ensure all inputs share same `name` (radios only)
- ✅ Check `min`/`max` attributes are numbers

### Phone Field Allows Letters
- ✅ Ensure `type="tel"` on input
- ✅ Check JavaScript initialization
- ✅ Test in different browsers (some mobile keyboards differ)

### Submit Always Fails (Spam Alert)
- ✅ Wait 5+ seconds after page load before testing
- ✅ This is intentional spam protection

### Icons Not Showing
- ✅ Check CSS for `.is--success` and `.is--error` classes
- ✅ Ensure icon elements exist in HTML
- ✅ Check z-index and positioning

---

## Browser Compatibility

**Tested and working in:**
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
- Mobile Safari (iOS 14+)
- Chrome Mobile (Android 10+)

**Key Dependencies:**
- `e.key` for keyboard events (ES6+)
- `requestAnimationFrame` for performance
- `classList` API for class manipulation

---

## Files

- `validation.html` - Complete form example with validation
- `default.html` - Basic form template
- `osmo.html` - Osmo-specific form implementation

---

## Questions?

For implementation questions or issues, contact your development team.

**Last Updated:** December 2025

Form Validation styles
1<style>
2    /* Field: Error State */
3    [data-validate].is--error input,
4    [data-validate].is--error textarea,
5    [data-validate].is--error select {
6        border-color: var(--form-validation--error-border);
7    }
8
9    /* Field: Error Icon Visibility */
10    [data-validate].is--error .form-field-icon.is--error,
11    [data-validate].is--error .radiocheck-field-icon.is--error {
12        opacity: 1;
13    }
14
15    /* Field: Success */
16    [data-validate].is--success input,
17    [data-validate].is--success textarea,
18    [data-validate].is--success select {
19        border-color: var(--form-validation--success-border);
20    }
21
22    /* Field: Success Icon Visibility */
23    [data-validate].is--success .form-field-icon.is--success,
24    [data-validate].is--success .radiocheck-field-icon.is--success {
25        opacity: 1;
26    }
27
28    /* Field: Custom Radio or Checkbox Focused State */
29    [data-form-validate] .radiocheck-field input:focus-visible~.radiocheck-custom {
30        background-color:var(--form-validation--radio-or-checkbox--focused-background);
31        color: var(--form-validation--radio-or-checkbox--focused-color);
32    }
33
34     /* Field: Custom Radio or Checkbox */
35    [data-form-validate] .radiocheck-field input:focus-visible:checked~.radiocheck-custom,
36    [data-form-validate] .radiocheck-field input:checked~.radiocheck-custom {
37        background-color: var(--form-validation--radio-or-checkbox--checked-background);  /* This is the checkbox tick / radio button bullet fill color */
38        color: var(--form-validation--radio-or-checkbox--checked-color) /* This is the checkbox tick / radio button bullet color */
39    }
40
41    [data-form-validate] .radiocheck-field .radiocheck-label.is--small {
42        margin-top: 0.125em;
43    }
44
45    /* Field: Custom Radio or Checkbox Error state */
46    [data-validate].is--error .radiocheck-custom {
47        border-color: var(--form-validation--error-border)
48    }
49
50    /* Field: Checkbox Error state but checked if there must be more then one selected*/
51    [data-validate].is--error input:checked~.radiocheck-custom {
52        border-color: var(--form-validation--error-border)
53    }
54
55    /* Field: Select before option is selected*/
56    [data-form-validate] select:has(option[value=""]:checked) {
57        color: rgba(19, 19, 19, 0.3);
58    }
59</style>
Form validation script
1<script>
2    function initAdvancedFormValidation() {
3        const forms = document.querySelectorAll('[data-form-validate]');
4
5        forms.forEach((formContainer) => {
6            const startTime = new Date().getTime();
7
8            const form = formContainer.querySelector('form');
9            if (!form) return;
10
11            const validateFields = form.querySelectorAll('[data-validate]');
12            const dataSubmit = form.querySelector('[data-submit]');
13            if (!dataSubmit) return;
14
15            const realSubmitInput = dataSubmit.querySelector('input[type="submit"]');
16            if (!realSubmitInput) return;
17
18            function isSpam() {
19                const currentTime = new Date().getTime();
20                return currentTime - startTime < 5000;
21            }
22
23            // Disable select options with invalid values on page load
24            validateFields.forEach(function (fieldGroup) {
25                const select = fieldGroup.querySelector('select');
26                if (select) {
27                    const options = select.querySelectorAll('option');
28                    options.forEach(function (option) {
29                        if (
30                            option.value === '' ||
31                            option.value === 'disabled' ||
32                            option.value === 'null' ||
33                            option.value === 'false'
34                        ) {
35                            option.setAttribute('disabled', 'disabled');
36                        }
37                    });
38                }
39            });
40
41            function validateAndStartLiveValidationForAll() {
42                let allValid = true;
43                let firstInvalidField = null;
44
45                validateFields.forEach(function (fieldGroup) {
46                    const input = fieldGroup.querySelector('input, textarea, select');
47                    const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
48                    if (!input && !radioCheckGroup) return;
49
50                    if (input) input.__validationStarted = true;
51                    if (radioCheckGroup) {
52                        radioCheckGroup.__validationStarted = true;
53                        const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
54                        inputs.forEach(function (input) {
55                            input.__validationStarted = true;
56                        });
57                    }
58
59                    updateFieldStatus(fieldGroup);
60
61                    if (!isValid(fieldGroup)) {
62                        allValid = false;
63                        if (!firstInvalidField) {
64                            firstInvalidField = input || radioCheckGroup.querySelector('input');
65                        }
66                    }
67                });
68
69                if (!allValid && firstInvalidField) {
70                    firstInvalidField.focus();
71                }
72
73                return allValid;
74            }
75
76            function isValid(fieldGroup) {
77                const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
78                if (radioCheckGroup) {
79                    const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
80                    const checkedInputs = radioCheckGroup.querySelectorAll('input:checked');
81                    const minAttr = radioCheckGroup.getAttribute('min');
82                    const min = minAttr !== null ? parseInt(minAttr) : 1;
83                    const max = parseInt(radioCheckGroup.getAttribute('max')) || inputs.length;
84                    const checkedCount = checkedInputs.length;
85
86                    if (inputs[0].type === 'radio') {
87                        return checkedCount >= min;
88                    } else {
89                        if (inputs.length === 1) {
90                            return inputs[0].checked;
91                        } else {
92                            return checkedCount >= min && checkedCount <= max;
93                        }
94                    }
95                } else {
96                    const input = fieldGroup.querySelector('input, textarea, select');
97                    if (!input) return false;
98
99                    let valid = true;
100                    const min = parseInt(input.getAttribute('min')) || 0;
101                    const max = parseInt(input.getAttribute('max')) || Infinity;
102                    const value = input.value.trim();
103                    const length = value.length;
104
105                    if (input.tagName.toLowerCase() === 'select') {
106                        if (
107                            value === '' ||
108                            value === 'disabled' ||
109                            value === 'null' ||
110                            value === 'false'
111                        ) {
112                            valid = false;
113                        }
114                    } else if (input.type === 'email') {
115                        const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
116                        valid = emailPattern.test(value);
117              } else if (input.type === 'tel') {
118                // Phone validation - supports both international and local formats
119                // Remove all non-digit characters except leading +
120                const cleanPhone = value.replace(/[^\d\+]/g, '');
121                
122                // Pattern: optional +, then 7-15 digits
123                // Local numbers: 7-10 digits (e.g., 5551234, 5551234567)
124                // International: + followed by country code and number (e.g., +1234567890)
125                const phonePattern = /^(\+?\d{7,15})$/;
126                
127                valid = phonePattern.test(cleanPhone) && cleanPhone.length >= 7;
128              } else {
129                        if (input.hasAttribute('min') && length < min) valid = false;
130                        if (input.hasAttribute('max') && length > max) valid = false;
131                    }
132
133                    return valid;
134                }
135            }
136
137            function updateFieldStatus(fieldGroup) {
138                const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
139                if (radioCheckGroup) {
140                    const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
141                    const checkedInputs = radioCheckGroup.querySelectorAll('input:checked');
142
143                    if (checkedInputs.length > 0) {
144                        fieldGroup.classList.add('is--filled');
145                    } else {
146                        fieldGroup.classList.remove('is--filled');
147                    }
148
149                    const valid = isValid(fieldGroup);
150
151                    if (valid) {
152                        fieldGroup.classList.add('is--success');
153                        fieldGroup.classList.remove('is--error');
154                    } else {
155                        fieldGroup.classList.remove('is--success');
156                        const anyInputValidationStarted = Array.from(inputs).some(input => input.__validationStarted);
157                        if (anyInputValidationStarted) {
158                            fieldGroup.classList.add('is--error');
159                        } else {
160                            fieldGroup.classList.remove('is--error');
161                        }
162                    }
163                } else {
164                    const input = fieldGroup.querySelector('input, textarea, select');
165                    if (!input) return;
166
167                    const value = input.value.trim();
168
169                    if (value) {
170                        fieldGroup.classList.add('is--filled');
171                    } else {
172                        fieldGroup.classList.remove('is--filled');
173                    }
174
175                    const valid = isValid(fieldGroup);
176
177                    if (valid) {
178                        fieldGroup.classList.add('is--success');
179                        fieldGroup.classList.remove('is--error');
180                    } else {
181                        fieldGroup.classList.remove('is--success');
182                        if (input.__validationStarted) {
183                            fieldGroup.classList.add('is--error');
184                        } else {
185                            fieldGroup.classList.remove('is--error');
186                        }
187                    }
188                }
189            }
190
191            validateFields.forEach(function (fieldGroup) {
192                const input = fieldGroup.querySelector('input, textarea, select');
193                const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
194
195                if (radioCheckGroup) {
196                    const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
197                    inputs.forEach(function (input) {
198                        input.__validationStarted = false;
199
200                        input.addEventListener('change', function () {
201                            requestAnimationFrame(function () {
202                                if (!input.__validationStarted) {
203                                    const checkedCount = radioCheckGroup.querySelectorAll('input:checked').length;
204                                    const minAttr = radioCheckGroup.getAttribute('min');
205                                    const min = minAttr !== null ? parseInt(minAttr) : 1;
206
207                                    // Start validation when min requirement is met, or immediately if min=0 (optional)
208                                    if (checkedCount >= min || min === 0) {
209                                        input.__validationStarted = true;
210                                    }
211                                }
212
213                                if (input.__validationStarted) {
214                                    updateFieldStatus(fieldGroup);
215                                }
216                            });
217                        });
218
219                        input.addEventListener('blur', function () {
220                            input.__validationStarted = true;
221                            updateFieldStatus(fieldGroup);
222                        });
223                    });
224                } else if (input) {
225                    input.__validationStarted = false;
226
227                    // Prevent typing letters in phone inputs
228                    if (input.type === 'tel') {
229                        input.addEventListener('keydown', function (e) {
230                            // Allow control keys
231                            if (e.key === 'Backspace' || 
232                                e.key === 'Delete' || 
233                                e.key === 'Tab' || 
234                                e.key === 'Escape' || 
235                                e.key === 'Enter' ||
236                                e.key === 'ArrowLeft' || 
237                                e.key === 'ArrowRight' || 
238                                e.key === 'ArrowUp' || 
239                                e.key === 'ArrowDown' ||
240                                e.key === 'Home' || 
241                                e.key === 'End') {
242                                return;
243                            }
244                            
245                            // Allow Ctrl/Cmd shortcuts (Copy, Paste, Cut, Select All)
246                            if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'c' || e.key === 'v' || e.key === 'x' || e.key === 'A' || e.key === 'C' || e.key === 'V' || e.key === 'X')) {
247                                return;
248                            }
249                            
250                            // Allow numbers, +, -, (, ), and space
251                            const allowedChars = /[0-9+\-() ]/;
252                            if (!allowedChars.test(e.key)) {
253                                e.preventDefault();
254                            }
255                        });
256
257                        // Additional safeguard: remove non-allowed characters on input (for paste)
258                        input.addEventListener('input', function (e) {
259                            const cursorPos = input.selectionStart;
260                            const oldValue = input.value;
261                            const newValue = oldValue.replace(/[^0-9+\-() ]/g, '');
262                            if (oldValue !== newValue) {
263                                input.value = newValue;
264                                // Restore cursor position
265                                input.setSelectionRange(cursorPos - 1, cursorPos - 1);
266                            }
267                        });
268                    }
269
270                    if (input.tagName.toLowerCase() === 'select') {
271                        input.addEventListener('change', function () {
272                            input.__validationStarted = true;
273                            updateFieldStatus(fieldGroup);
274                        });
275                    } else {
276                        // Validation input listener (separate from tel character filter)
277                        input.addEventListener('input', function () {
278                            const value = input.value.trim();
279                            const length = value.length;
280                            const min = parseInt(input.getAttribute('min')) || 0;
281                            const max = parseInt(input.getAttribute('max')) || Infinity;
282
283                            if (!input.__validationStarted) {
284                                if (input.type === 'email') {
285                                    if (isValid(fieldGroup)) input.__validationStarted = true;
286                                } else if (input.type === 'tel') {
287                                    if (isValid(fieldGroup)) input.__validationStarted = true;
288                                } else {
289                                    if (
290                                        (input.hasAttribute('min') && length >= min) ||
291                                        (input.hasAttribute('max') && length <= max)
292                                    ) {
293                                        input.__validationStarted = true;
294                                    }
295                                }
296                            }
297
298                            if (input.__validationStarted) {
299                                updateFieldStatus(fieldGroup);
300                            }
301                        }, { capture: false });
302
303                        input.addEventListener('blur', function () {
304                            input.__validationStarted = true;
305                            updateFieldStatus(fieldGroup);
306                        });
307                    }
308                }
309            });
310
311            dataSubmit.addEventListener('click', function () {
312                if (validateAndStartLiveValidationForAll()) {
313                    if (isSpam()) {
314                        alert('Form submitted too quickly. Please try again.');
315                        return;
316                    }
317                    realSubmitInput.click();
318                }
319            });
320
321            form.addEventListener('keydown', function (event) {
322                if (event.key === 'Enter' && event.target.tagName !== 'TEXTAREA') {
323                    event.preventDefault();
324                    if (validateAndStartLiveValidationForAll()) {
325                        if (isSpam()) {
326                            alert('Form submitted too quickly. Please try again.');
327                            return;
328                        }
329                        realSubmitInput.click();
330                    }
331                }
332            });
333        });
334    }
335
336    // Initialize Advanced Form Validation
337    document.addEventListener('DOMContentLoaded', () => {
338        initAdvancedFormValidation();
339    });
340</script>