diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6cc621fc..bcaec28d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
       - name: Install Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: 3.3.4
+          ruby-version: 3.4.1
       - name: Cache gems
         uses: actions/cache@v1
         with:
@@ -35,7 +35,7 @@ jobs:
       - name: Install Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: 3.3.4
+          ruby-version: 3.4.1
       - name: Cache gems
         uses: actions/cache@v1
         with:
@@ -100,7 +100,7 @@ jobs:
       - name: Install Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: 3.3.4
+          ruby-version: 3.4.1
       - name: Set up Node
         uses: actions/setup-node@v1
         with:
diff --git a/Dockerfile b/Dockerfile
index fed21511..cfabc332 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:3.3.4-alpine as fonts
+FROM ruby:3.4.1-alpine as fonts
 
 WORKDIR /fonts
 
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
 
 RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
 
-FROM ruby:3.3.4-alpine as webpack
+FROM ruby:3.4.1-alpine as webpack
 
 ENV RAILS_ENV=production
 ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
 
 RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
 
-FROM ruby:3.3.4-alpine as app
+FROM ruby:3.4.1-alpine as app
 
 ENV RAILS_ENV=production
 ENV BUNDLE_WITHOUT="development:test"
diff --git a/Gemfile b/Gemfile
index 4151d8ac..39cf1bea 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,7 +2,7 @@
 
 source 'https://rubygems.org'
 
-ruby '3.3.4'
+ruby '3.4.1'
 
 gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
 gem 'aws-sdk-s3', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index f32faeb7..7f37cb0c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -634,7 +634,7 @@ DEPENDENCIES
   webmock
 
 RUBY VERSION
-   ruby 3.3.4p94
+   ruby 3.4.1p0
 
 BUNDLED WITH
    2.5.3
diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb
index 08b2ed02..cf2d0097 100644
--- a/app/controllers/api/submissions_controller.rb
+++ b/app/controllers/api/submissions_controller.rb
@@ -178,7 +178,7 @@ module Api
         key = params.key?(:submission) ? :submission : :submissions
 
         params.permit(
-          key => [permitted_attrs]
+          { key => [permitted_attrs] }, { key => permitted_attrs }
         ).fetch(key, [])
       end
     end
diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue
index db5d2783..61b1a200 100644
--- a/app/javascript/template_builder/area.vue
+++ b/app/javascript/template_builder/area.vue
@@ -2,6 +2,7 @@
   
 1 - this.area.h)) {
+        this.area.y = Math.min(Math.max(this.area.y, 0), 1 - this.area.h)
+      }
     },
     stopTouchDrag () {
       this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
       this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
 
+      this.maybeChangeAreaPage(this.area)
+
       if (this.isDragged) {
         this.save()
       }
@@ -773,12 +785,18 @@ export default {
       const rect = page.getBoundingClientRect()
 
       this.area.x = Math.min(Math.max((this.dragFrom.x + e.clientX - rect.left) / rect.width, 0), 1 - this.area.w)
-      this.area.y = Math.min(Math.max((this.dragFrom.y + e.clientY - rect.top) / rect.height, 0), 1 - this.area.h)
+      this.area.y = (this.dragFrom.y + e.clientY - rect.top) / rect.height
+
+      if ((this.area.page === 0 && this.area.y < 0) || (this.area.page === this.maxPage && this.area.y > 1 - this.area.h)) {
+        this.area.y = Math.min(Math.max(this.area.y, 0), 1 - this.area.h)
+      }
     },
     stopMouseMove (e) {
       this.$el.getRootNode().removeEventListener('mousemove', this.mouseMove)
       this.$el.getRootNode().removeEventListener('mouseup', this.stopMouseMove)
 
+      this.maybeChangeAreaPage(this.area)
+
       if (this.isMoved) {
         this.save()
       }
@@ -788,6 +806,15 @@ export default {
 
       this.$emit('stop-drag')
     },
+    maybeChangeAreaPage (area) {
+      if (area.y < -(area.h / 2)) {
+        area.page -= 1
+        area.y = 1 + area.y + (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight)
+      } else if (area.y > 1 - (area.h / 2)) {
+        area.page += 1
+        area.y = area.y - 1 - (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight)
+      }
+    },
     stopDrag () {
       this.$el.getRootNode().removeEventListener('mousemove', this.drag)
       this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
diff --git a/app/javascript/template_builder/document.vue b/app/javascript/template_builder/document.vue
index a659f33a..33ac6adc 100644
--- a/app/javascript/template_builder/document.vue
+++ b/app/javascript/template_builder/document.vue
@@ -16,6 +16,7 @@
       :draw-field="drawField"
       :draw-field-type="drawFieldType"
       :selected-submitter="selectedSubmitter"
+      :total-pages="sortedPreviewImages.length"
       :image="image"
       @drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
       @remove-area="$emit('remove-area', $event)"
diff --git a/app/javascript/template_builder/page.vue b/app/javascript/template_builder/page.vue
index 27fc116e..bd5e22f9 100644
--- a/app/javascript/template_builder/page.vue
+++ b/app/javascript/template_builder/page.vue
@@ -27,6 +27,7 @@
         :with-field-placeholder="withFieldPlaceholder"
         :default-field="defaultFields.find((f) => f.name === item.field.name)"
         :default-submitters="defaultSubmitters"
+        :max-page="totalPages - 1"
         @start-resize="resizeDirection = $event"
         @stop-resize="resizeDirection = null"
         @remove="$emit('remove-area', item.area)"
@@ -88,6 +89,10 @@ export default {
       required: false,
       default: false
     },
+    totalPages: {
+      type: Number,
+      required: true
+    },
     drawFieldType: {
       type: String,
       required: false,
diff --git a/lib/submissions/generate_preview_attachments.rb b/lib/submissions/generate_preview_attachments.rb
index f03f689f..742f96f4 100644
--- a/lib/submissions/generate_preview_attachments.rb
+++ b/lib/submissions/generate_preview_attachments.rb
@@ -26,9 +26,9 @@ module Submissions
                      submission.submitters.where(completed_at: nil)
                    end
 
-      submitters.preload(attachments_attachments: :blob).each do |s|
+      submitters.preload(attachments_attachments: :blob).each_with_index do |s, index|
         GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
-                                                        with_signature_id:, is_flatten:)
+                                                        with_signature_id:, is_flatten:, with_headings: index.zero?)
       end
 
       template = submission.template
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index 1c02c546..1686d7ec 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -147,15 +147,18 @@ module Submissions
       fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:)
     end
 
-    def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:)
+    def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil)
       cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
 
       attachments_data_cache = {}
 
       return pdfs_index if submitter.submission.template_fields.blank?
 
+      with_headings = find_last_submitter(submitter.submission, submitter:).blank? if with_headings.nil?
+
       submitter.submission.template_fields.each do |field|
-        next if field['submitter_uuid'] != submitter.uuid
+        next if field['type'] == 'heading' && !with_headings
+        next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading'
 
         field.fetch('areas', []).each do |area|
           pdf = pdfs_index[area['attachment_uuid']]
@@ -188,6 +191,7 @@ module Submissions
           font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
 
           value = submitter.values[field['uuid']]
+          value = field['default_value'] if field['type'] == 'heading'
 
           text_align = field.dig('preferences', 'align').to_s.to_sym.presence ||
                        (value.to_s.match?(RTL_REGEXP) ? :right : :left)
diff --git a/spec/signing_form_helper.rb b/spec/signing_form_helper.rb
index 13aadfc2..6d86484b 100644
--- a/spec/signing_form_helper.rb
+++ b/spec/signing_form_helper.rb
@@ -19,7 +19,7 @@ module SigningFormHelper
       ctx.lineTo(150, 150);
       ctx.stroke();
     JS
-    sleep 1
+    sleep 0.5
   end
 
   def field_value(submitter, field_name)
diff --git a/spec/system/setup_spec.rb b/spec/system/setup_spec.rb
index 7c38aa17..25c6df27 100644
--- a/spec/system/setup_spec.rb
+++ b/spec/system/setup_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'App Setup' do
 
       expect do
         click_button 'Submit'
-        sleep 2
+        page.driver.wait_for_network_idle
       end.to change(Account, :count).by(1).and change(User, :count).by(1).and change(EncryptedConfig, :count).by(2)
 
       user = User.last