diff --git a/Gemfile.lock b/Gemfile.lock
index 1d12bddd212637262206680f8835295343f1fbbe..fa9ba26b2fa9d5ca453aa167b0301f0380cda6e3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -383,6 +383,7 @@ PLATFORMS
   aarch64-linux
   arm64-darwin-22
   arm64-darwin-23
+  arm64-darwin-24
   x86_64-linux
 
 DEPENDENCIES
@@ -414,4 +415,4 @@ RUBY VERSION
    ruby 3.4.3p32
 
 BUNDLED WITH
-   2.6.2
+   2.6.7
diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb
index 8731d681f7edb2a190897fa09a1d8662c4a32d04..0c20e1812432e6f4cdb60bf3bbb5c81b4d45ea4a 100644
--- a/app/controllers/assignments_controller.rb
+++ b/app/controllers/assignments_controller.rb
@@ -97,96 +97,13 @@ class AssignmentsController < ApplicationController
     respond_to do |format|
       format.html # This will render the existing HTML view (assignments/by_user.html.erb)
 
-      format.ics do
-        calendar = Icalendar::Calendar.new
-        tz = TZInfo::Timezone.get("UTC")
-        calendar.add_timezone tz.ical_timezone Time.now
-
-        # Add confirmed assignments from active conferences only
-        @user.assignments.joins(session: :conference).where(conferences: { active: true }).each do |assignment|
-          session = assignment.session
-          assignees = session.assignments.map { |a| a.user.name }
-          desc = [
-            "Assignees: #{assignees.join(', ')}",
-            "Speakers: #{session.speakers.map(&:name).join(', ')}",
-            session.description
-          ]
-          desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop?
-
-          event = Icalendar::Event.new
-          event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name)
-          event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name)
-          event.summary = [ session.title, session.stage.name ].join(" @ ")
-          event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n")
-          event.location = [ session.stage.name, session.conference.name ].join(" @ ")
-          event.created = Icalendar::Values::DateTime.new(session.created_at)
-          event.last_modified = Icalendar::Values::DateTime.new(session.updated_at)
-          event.uid = [ session.conference.slug, session.ref_id ].join("-")
-          event.status = "CONFIRMED"
-          event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>"))
-          calendar.add_event(event)
-        end
-
-        # Add candidate sessions from active conferences if feature flag is enabled
-        if include_candidate_sessions?
-          @user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session).each do |candidate|
-            session = candidate.session
-            # Skip if user is already assigned to this session
-            next if @user.assignments.any? { |a| a.session_id == session.id }
-
-            assignees = session.assignments.map { |a| a.user.name }
-            candidates = session.candidates.map { |c| c.user.name }
-            desc = [
-              "🤔 CANDIDATE SESSION - You've expressed interest in this session",
-              "Current Assignees: #{assignees.any? ? assignees.join(', ') : 'None yet'}",
-              "Other Candidates: #{candidates.reject { |name| name == @user.name }.join(', ')}",
-              "Speakers: #{session.speakers.map(&:name).join(', ')}",
-              session.description
-            ]
-            desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop?
-
-            event = Icalendar::Event.new
-            event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name)
-            event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name)
-            event.summary = "[CANDIDATE] #{session.title} @ #{session.stage.name}"
-            event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n")
-            event.location = [ session.stage.name, session.conference.name ].join(" @ ")
-            event.created = Icalendar::Values::DateTime.new(session.created_at)
-            event.last_modified = Icalendar::Values::DateTime.new(session.updated_at)
-            event.uid = [ session.conference.slug, session.ref_id, "candidate" ].join("-")
-            event.status = "TENTATIVE"
-            event.transp = "TRANSPARENT" # Shows as "free" time in most calendar apps
-            event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>"))
-            calendar.add_event(event)
-          end
-        end
-
-        # Add standby assignments from active conferences
-        @user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).each do |standby_assignment|
-          standby_block = standby_assignment.standby_block
-          other_assignees = standby_block.users.where.not(id: @user.id).map(&:name)
-
-          desc = [
-            "🛡️ STANDBY BLOCK - You are on standby for translation needs",
-            "Conference: #{standby_block.conference.name}",
-            ("Other standby translators: #{other_assignees.join(', ')}" if other_assignees.any?),
-            ("Notes: #{standby_block.notes}" if standby_block.notes.present?)
-          ].compact.reject(&:blank?)
-
-          event = Icalendar::Event.new
-          event.dtstart = Icalendar::Values::DateTime.new(standby_block.starts_at, tzid: standby_block.conference.time_zone || "UTC")
-          event.dtend = Icalendar::Values::DateTime.new(standby_block.ends_at, tzid: standby_block.conference.time_zone || "UTC")
-          event.summary = "[STANDBY] Standby Block @ #{standby_block.conference.name}"
-          event.description = desc.join("\n\n")
-          event.location = standby_block.conference.name
-          event.created = Icalendar::Values::DateTime.new(standby_assignment.created_at)
-          event.last_modified = Icalendar::Values::DateTime.new(standby_assignment.updated_at)
-          event.uid = [ standby_block.conference.slug, "standby", standby_block.id ].join("-")
-          event.status = "CONFIRMED"
-          event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>"))
-          calendar.add_event(event)
-        end
+      format.ics do |variant|
+        # Determine if candidates should be included based on the action name or a parameter
+        # For the standard .ics feed, candidates are NOT included.
+        # A new action/route (e.g., by_user_with_candidates) will set params[:include_candidates]
+        should_include_candidates = params[:include_candidates] == "true" && include_candidate_sessions?
 
+        calendar = build_ical_for_user(@user, include_candidates: should_include_candidates)
         calendar.publish
         headers["Content-Type"] = "text/calendar; charset=UTF-8"
         render plain: calendar.to_ical
@@ -194,8 +111,137 @@ class AssignmentsController < ApplicationController
     end
   end
 
+  # Potentially a new action for the candidates feed, or reuse by_user with a param
+  # For simplicity, let's assume we add a new route that points to by_user
+  # and sets a param to differentiate. Or, we can create a new action.
+  # Let's create a new action for clarity, though it will largely reuse by_user's logic.
+
+  def by_user_with_candidates
+    @user = User.find(params[:user_id])
+    # Ensure data is loaded similar to by_user if needed for other formats,
+    # but for .ics, build_ical_for_user will handle queries.
+    @active_assignments = @user.assignments.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [ :conference, :stage ])
+    @active_standby_assignments = @user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).includes(standby_block: :conference)
+    if include_candidate_sessions?
+      @active_candidates = @user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [ :conference, :stage ])
+    end
+
+    respond_to do |format|
+      format.ics do
+        if include_candidate_sessions?
+          calendar = build_ical_for_user(@user, include_candidates: true)
+          calendar.publish
+          headers["Content-Type"] = "text/calendar; charset=UTF-8"
+          render plain: calendar.to_ical
+        else
+          # Or render an empty calendar, or raise an error, or redirect.
+          # For now, let's render an empty calendar if the feature is off but this endpoint is hit.
+          calendar = Icalendar::Calendar.new
+          calendar.publish
+          headers["Content-Type"] = "text/calendar; charset=UTF-8"
+          render plain: calendar.to_ical, status: :forbidden # Or :not_found
+        end
+      end
+      # Potentially handle .html differently or redirect if direct access to this action via HTML is not desired
+      format.html { redirect_to by_user_assignments_path(@user), notice: "To include candidates, ensure the feature is enabled and use the specific iCal link." }
+    end
+  end
+
+
   private
 
+  def build_ical_for_user(user, include_candidates: false)
+    calendar = Icalendar::Calendar.new
+    tz = TZInfo::Timezone.get("UTC") # Or user.time_zone if available and relevant
+    calendar.add_timezone tz.ical_timezone Time.now
+
+    # Add confirmed assignments from active conferences only
+    user.assignments.joins(session: :conference).where(conferences: { active: true }).each do |assignment|
+      session = assignment.session
+      assignees = session.assignments.map { |a| a.user.name }
+      desc = [
+        "Assignees: #{assignees.join(', ')}",
+        "Speakers: #{session.speakers.map(&:name).join(', ')}",
+        session.description
+      ]
+      desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop?
+
+      event = Icalendar::Event.new
+      event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name)
+      event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name)
+      event.summary = [ session.title, session.stage.name ].join(" @ ")
+      event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n")
+      event.location = [ session.stage.name, session.conference.name ].join(" @ ")
+      event.created = Icalendar::Values::DateTime.new(session.created_at)
+      event.last_modified = Icalendar::Values::DateTime.new(session.updated_at)
+      event.uid = [ session.conference.slug, session.ref_id ].join("-")
+      event.status = "CONFIRMED"
+      event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>"))
+      calendar.add_event(event)
+    end
+
+    # Add candidate sessions from active conferences if requested and feature flag is enabled
+    if include_candidates && include_candidate_sessions?
+      user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session).each do |candidate|
+        session = candidate.session
+        # Skip if user is already assigned to this session
+        next if user.assignments.any? { |a| a.session_id == session.id }
+
+        assignees = session.assignments.map { |a| a.user.name }
+        candidates_list = session.candidates.map { |c| c.user.name }
+        desc = [
+          "🤔 CANDIDATE SESSION - You've expressed interest in this session",
+          "Current Assignees: #{assignees.any? ? assignees.join(', ') : 'None yet'}",
+          "Other Candidates: #{candidates_list.reject { |name| name == user.name }.join(', ')}",
+          "Speakers: #{session.speakers.map(&:name).join(', ')}",
+          session.description
+        ]
+        desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop?
+
+        event = Icalendar::Event.new
+        event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name)
+        event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name)
+        event.summary = "[CANDIDATE] #{session.title} @ #{session.stage.name}"
+        event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n")
+        event.location = [ session.stage.name, session.conference.name ].join(" @ ")
+        event.created = Icalendar::Values::DateTime.new(session.created_at)
+        event.last_modified = Icalendar::Values::DateTime.new(session.updated_at)
+        event.uid = [ session.conference.slug, session.ref_id, "candidate" ].join("-")
+        event.status = "TENTATIVE"
+        event.transp = "TRANSPARENT" # Shows as "free" time in most calendar apps
+        event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>"))
+        calendar.add_event(event)
+      end
+    end
+
+    # Add standby assignments from active conferences
+    user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).each do |standby_assignment|
+      standby_block = standby_assignment.standby_block
+      other_assignees = standby_block.users.where.not(id: user.id).map(&:name)
+
+      desc = [
+        "🛡️ STANDBY BLOCK - You are on standby for translation needs",
+        "Conference: #{standby_block.conference.name}",
+        ("Other standby translators: #{other_assignees.join(', ')}" if other_assignees.any?),
+        ("Notes: #{standby_block.notes}" if standby_block.notes.present?)
+      ].compact.reject(&:blank?)
+
+      event = Icalendar::Event.new
+      event.dtstart = Icalendar::Values::DateTime.new(standby_block.starts_at, tzid: standby_block.conference.time_zone || "UTC")
+      event.dtend = Icalendar::Values::DateTime.new(standby_block.ends_at, tzid: standby_block.conference.time_zone || "UTC")
+      event.summary = "[STANDBY] Standby Block @ #{standby_block.conference.name}"
+      event.description = desc.join("\n\n")
+      event.location = standby_block.conference.name
+      event.created = Icalendar::Values::DateTime.new(standby_assignment.created_at)
+      event.last_modified = Icalendar::Values::DateTime.new(standby_assignment.updated_at)
+      event.uid = [ standby_block.conference.slug, "standby", standby_block.id ].join("-")
+      event.status = "CONFIRMED"
+      event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>"))
+      calendar.add_event(event)
+    end
+    calendar
+  end
+
   def authorize_permission
     super("manage_assignments")
   end
diff --git a/app/views/assignments/by_user.html.erb b/app/views/assignments/by_user.html.erb
index 05c1140c3cf2c671ea02c1af949c95e61a223d80..4fb3049b96fd65880cd7329e433bf3f4f3e2b808 100644
--- a/app/views/assignments/by_user.html.erb
+++ b/app/views/assignments/by_user.html.erb
@@ -7,10 +7,15 @@
           <%= link_to @user.name, user_assignments_path(@user), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %>
         </h1>
         <%= link_to user_assignments_path(@user, format: 'ics'), class: "btn btn-info-light ml-4" do %>
-          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal
+          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal (Confirmed)
+        <% end %>
+        <% if @controller.send(:include_candidate_sessions?) %>
+          <%= link_to user_with_candidates_assignments_path(@user, format: 'ics'), class: "btn btn-warning-light ml-2" do %>
+            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal (Incl. Unconfirmed)
+          <% end %>
         <% end %>
       </div>
-      <% if @active_candidates %>
+      <% if @active_candidates && @controller.send(:include_candidate_sessions?) %>
         <div class="flex items-center">
           <label class="inline-flex items-center cursor-pointer">
             <input type="checkbox" id="showCandidates" class="sr-only peer" checked>
diff --git a/config/routes.rb b/config/routes.rb
index 6740c3ea6f30250f102ed03446229347ebce6b6d..415e150a0432330dfca6821757ee0021be7cd0ab 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -74,6 +74,7 @@ Rails.application.routes.draw do
 
   resources :assignments, only: [ :index ] do
     get "for/:user_id", action: "by_user", on: :collection, as: :user
+    get "for/:user_id/with_candidates", action: "by_user_with_candidates", on: :collection, as: :user_with_candidates
   end
   resources :sessions, param: :ref_id