draw_title = function(main, sub, cap, xlab, ylab, legend, legend_args, opar,
                      xlab_line_offset = 0,
                      ylab_line_offset = 0) {
  # main title
  # Note that we include a special catch for the main title if legend is
  # "top!" (and main is specified in the first place).
  legend_eval = tryCatch(eval(legend), error = function(e) NULL)
  # Extra bit of footwork if user passed legend = legend(...) instead of
  # legend = list(...), since the call environment is tricky
  if (is.null(legend_eval)) {
    legend_eval = tryCatch(paste0(legend)[[2]], error = function(e) NULL)
  }

  adj_title = !is.null(legend) && ((is.character(legend) && legend == "top!") || (!is.null(legend_args[["x"]]) && legend_args[["x"]] == "top!") || (is.list(legend_eval) && legend_eval[[1]] == "top!"))

  # For the "top!" legend case, bump main title up to make space for the
  # legend beneath it: Take the normal main title line gap (i.e., 1.7 lines)
  # and add the difference between original top margin and new one (i.e.,
  # which should equal the height of the new legend). Note that we also
  # include a 0.1 epsilon bump, which we're using to reset the tinyplot
  # window in case of recursive "top!" calls. (See draw_legend code.)

  if (isTRUE(adj_title) && isTRUE(get_tpar("dynmar", FALSE))) {
    gap_main = get_tpar("gap.main", 0.7)
    top_soma = get_environment_variable(".top_legend_soma") %||% 0
    line_main = top_soma + gap_main - 0.1
  } else if (isTRUE(adj_title)) {
    line_main = par("mar")[3] - opar[["mar"]][3] + 1.7 + 0.1
  } else if (isTRUE(get_tpar("dynmar", FALSE))) {
    gap_main = get_tpar("gap.main", 0.7)
    line_main = get_tpar("mgp")[3] + gap_main - 0.1
  } else {
    line_main = NULL
  }

  # When sub sits on top (side.sub == 3), push main up by the sub block
  # height so main is above sub rather than overlapping it.
  sub_lines = text_line_count(sub)
  if (sub_lines > 0L && isTRUE(get_tpar("side.sub", 1) == 3)) {
    if (is.null(line_main)) {
      gap_main = get_tpar("gap.main", 0.7)
      line_main = get_tpar("mgp")[3] + gap_main - 0.1
    }
    cex_sub = get_tpar("cex.sub", 1.2)
    gap_sub = get_tpar("gap.sub", 0.7)
    line_main = line_main + gap_sub + (sub_lines - 1 + 0.6) * cex_sub
  }

  if (!is.null(sub)) {
    if (isTRUE(get_tpar("side.sub", 1) == 3)) {
      gap_sub = get_tpar("gap.sub", 0.7)
      line_sub = get_tpar("line.sub", gap_sub - 0.1)
      if (isTRUE(adj_title) && isTRUE(get_tpar("dynmar", FALSE))) {
        top_soma = get_environment_variable(".top_legend_soma") %||% 0
        line_sub = top_soma + gap_sub - 0.1
      }
    } else {
      line_sub = get_tpar("line.sub", 4)
    }
    args = list(
      text = sub,
      line = line_sub,
      cex = get_tpar("cex.sub", 1.2),
      col = get_tpar("col.sub", "black"),
      adj = get_tpar(c("adj.sub", "adj")),
      font = get_tpar("font.sub", 1),
      side = get_tpar("side.sub", 1),
      las = 1
    )
    args = Filter(function(x) !is.null(x), args)
    do.call(mtext, args, quote = TRUE)
  }

  if (!is.null(main)) {
    # title() stacks multi-line main *above* `line_main` (line N at
    # `line_main`, extra lines extend upward). The reserved top margin
    # already accounts for (N-1)*cex_main extra lines, so no line-shift
    # adjustment is needed here.
    # For "top!" + dynmar, line_main exceeds par("mar")[3] so we need xpd=NA
    # to allow drawing outside the clipping region.
    .oxpd = par("xpd")
    if (!is.null(line_main) && line_main > par("mar")[3] - 1) par(xpd = NA)
    args = list(
      main = main,
      line = line_main,
      cex.main = get_tpar("cex.main", 1.4),
      col.main = get_tpar("col.main", "black"),
      font.main = get_tpar("font.main", 2),
      adj = get_tpar(c("adj.main", "adj"), 3))
    args = Filter(function(x) !is.null(x), args)
    do.call(title, args, quote = TRUE)
    par(xpd = .oxpd)
  }


  cap_in_legend = !is.null(legend_args[["x"]]) && grepl("bottom!$", legend_args[["x"]])
  if (!is.null(cap) && !cap_in_legend) {
    cex_cap = get_tpar("cex.cap", 1)
    line_cap = get_tpar("line.cap", NULL)
    if (is.null(line_cap)) {
      line_cap = par("mar")[1] - 1
    }
    args = list(
      text = cap,
      line = line_cap,
      cex = cex_cap,
      col = get_tpar("col.cap", "black"),
      adj = get_tpar(c("adj.cap", "adj")),
      font = get_tpar("font.cap", 1),
      side = 1,
      las = 1
    )
    args = Filter(function(x) !is.null(x), args)
    do.call(mtext, args, quote = TRUE)
  }

  # Axis titles. For multi-line labels, base R places line 1 at
  # `line = mgp[1] - (N-1)*cex`, which pushes line 1 up into the tick-label
  # zone. Shift `line` down so line 1 aligns with where a single-line xlab
  # would be (and the extra lines extend below).
  # Also push down by xlab_line_offset (= .whtsbp[1]) when dynmar has
  # reserved extra space for rotated (las=2/3) x-tick labels.
  args = list(xlab = xlab)
  xlab_lines = text_line_count(xlab)
  cex_xlab = get_tpar(c("cex.xlab", "cex.lab"), 1)
  # Compute expression height excess (same logic as dynmar_side)
  xlab_expr_excess = 0
  if (is.language(xlab)) {
    expr_lines = strheight(xlab, units = "inches", cex = cex_xlab) / par("csi")
    text_lines = strheight("X", units = "inches", cex = cex_xlab) / par("csi")
    xlab_expr_excess = max(0, expr_lines - text_lines)
  }
  if (xlab_lines > 1L || xlab_line_offset != 0 || xlab_expr_excess > 0) {
    args[["line"]] = get_tpar("mgp")[1] +
                     (xlab_lines - 1) * cex_xlab +
                     xlab_line_offset +
                     xlab_expr_excess
  }
  args[["adj"]] = get_tpar(c("adj.xlab", "adj"))
  args[["cex.lab"]] = cex_xlab
  do.call(title, args, quote = TRUE)

  # ylab: base R already places multi-line text correctly (outermost line at
  # mgp[1], subsequent lines closer to the plot), so no line shift needed.
  args = list(ylab = ylab)
  cex_ylab = get_tpar(c("cex.ylab", "cex.lab"), 1)
  if (ylab_line_offset != 0) {
    args[["line"]] = get_tpar("mgp")[1] + ylab_line_offset
  }
  args[["adj"]] = get_tpar(c("adj.ylab", "adj"))
  args[["cex.lab"]] = cex_ylab
  do.call(title, args, quote = TRUE)
}
