Demographics

Gender

df_cbzs_elg %>% 
  mutate(gender = ifelse(is.na(gender) | gender == "","other",gender)) %>% 
  group_by(gender) %>% 
  summarise(N = n()) %>% 
  ungroup() %>% 
  mutate(Perc = round(100*(N/sum(N)),2)) %>% 
  ungroup() %>% 
  arrange(desc(Perc)) %>% 
  kbl() %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
gender N Perc
woman 267 55.05
man 210 43.30
other 8 1.65

Race

race N Perc
White 354 72.99
Black or African American 45 9.28
multiracial 34 7.01
Hispanic, Latino, or Spanish origin 27 5.57
Asian 20 4.12
American Indian or Alaska Native 1 0.21
Middle Eastern or North African 1 0.21
Native Hawaiian or Other Pacific Islander 1 0.21
Other (please specify) 1 0.21
NA 1 0.21

Age

Mean age: 39.98.

Income

median_income_num <- df_cbzs_elg %>% 
  mutate(income_num = as.numeric(income)) %>% 
  summarise(median = median(income_num, na.rm = TRUE)) %>% 
  pull(median)

df_cbzs_elg %>% 
  ggplot(aes(x = income)) +
  geom_bar() +
  geom_vline(xintercept = median_income_num, 
             color = "lightblue", linetype = "dashed") +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black", face = "bold"),
        axis.title.x = element_blank(),
        axis.title.y = element_blank()) +
  coord_flip()

Education

edu N Perc
GED 278 57.32
2yearColl 170 35.05
4yearColl 30 6.19
MA 3 0.62
PHD 1 0.21
NA 3 0.62

SES

ses N Perc
Lower Class 98 20.21
Lower Middle Class 183 37.73
Middle Class 177 36.49
Upper Middle Class 24 4.95
Upper Class 3 0.62

Politics

Political ideology

Participants were asked about the extent to which they subscribe to the following ideologies on a scale of 1-7 (select NA if unfamiliar): Conservatism, Liberalism, Democratic Socialism, Libertarianism, Progressivism.

means <- df_cbzs_elg %>%
  dplyr::select(PID,ideo_con:ideo_prog) %>% 
  pivot_longer(-PID,
               names_to = "ideo",
               values_to = "score") %>% 
  filter(!is.na(score)) %>% 
  group_by(ideo) %>% 
  summarise(score = mean(score)) %>% 
  ungroup()

df_cbzs_elg %>%
  dplyr::select(PID,ideo_con:ideo_prog) %>% 
  pivot_longer(-PID,
               names_to = "ideo",
               values_to = "score") %>% 
  filter(!is.na(score)) %>%  
  ggplot() +
  geom_density(aes(x = score), fill = "lightblue",color = NA) +
  scale_x_continuous(limits = c(1,7),
                     breaks = seq(1,7,1)) +
  geom_vline(data = means,mapping = aes(xintercept = score),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold")) +
  facet_wrap(~ideo,nrow = 2)

Party affiliation

party_id N Perc
Independent 174 35.88
Democrat 166 34.23
Republican 145 29.90

Measures

Class-based Zero-Sum Beliefs

  1. If the upper class becomes richer, this comes at the expense of the working class
  2. If the upper class makes more money, then the working class makes less money
  3. If the upper class does better economically, this does NOT come at the expense of the working class [R]

alpha = 0.9

df_cbzs_elg %>% 
  ggplot(aes(x = zs_class)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$zs_class,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Relative Deprivation

  1. When I think about what I have compared to others, I feel deprived
  2. I feel privileged compared to other people like me [R]
  3. I feel resentful when I see how prosperous other people seem to be
  4. When I compare what I have with others, I realize that I am quite well off [R]

alpha = 0.75

df_cbzs_elg %>% 
  ggplot(aes(x = reldep)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$reldep,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Perceived working-class disadvantage

  1. Working-class people are struggling to make ends meet.
  2. Most working-class people live paycheck to paycheck.
  3. Working-class people are facing serious financial hardship.

alpha = 0.93

df_cbzs_elg %>% 
  ggplot(aes(x = dis)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$dis,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

System justification

  1. In general, I find society to be fair.
  2. In general, the American political system operates as it should.
  3. American society needs to be radically restructured. [R]
  4. The United States is the best country in the world to live in.
  5. Most policies serve the greater good.
  6. Everyone has a fair shot at wealth and happiness.
  7. Our society is getting worse every year. [R]
  8. Society is set up so that people usually get what they deserve.

alpha = 0.9

df_cbzs_elg %>% 
  ggplot(aes(x = sj)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$sj,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

General zero-sum mindset

  1. The success of one person is usually the failure of another person.
  2. Life is such that when one person gains, someone else has to lose.
  3. When someone does much for others, they lose.
  4. In most situations, different people’s interests are incompatible.
  5. When one person is winning, it does not mean that someone else is losing. [R]
  6. Life is like a tennis game - A person wins only when another person loses.
  7. One person’s success is not another person’s failure. [R]

alpha = 0.88

df_cbzs_elg %>% 
  ggplot(aes(x = zsm)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$zsm,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Social dominance orientation

  1. An ideal society requires some groups to be on top and others to be on the bottom.
  2. Some groups of people are simply inferior to other groups.
  3. No one group should dominate in society. [R]
  4. Groups at the bottom are just as deserving as groups at the top. [R]
  5. Group equality should not be our primary goal.
  6. It is unjust to try to make groups equal.
  7. We should do what we can to equalize conditions for different groups. [R]
  8. We should work to give all groups an equal chance to succeed. [R]

alpha = 0.91

df_cbzs_elg %>% 
  ggplot(aes(x = SDO)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$SDO,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Class Solidarity

  1. I feel a sense of solidarity with the working class
  2. I support policy that helps the working class
  3. I stand united with the working class
  4. Policies negatively affecting the working class should be changed
  5. More people should know about how the working class are negatively affected by economic issues
  6. It’s important to challenge the power structures that disadvantage the working class

alpha = 0.89

df_cbzs_elg %>% 
  ggplot(aes(x = soli)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$soli,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Linked fate

Mean score of the out-group items in the following scale (we exclude each participant’s own self-reported in-group). If they are not affiliated with any of the following racial groups, we take the mean score of all items. If they are affiliated with more than one group, we take only the items of the groups they are not affiliated with.

  1. What happens to working-class White people in this country will have something to do with what happens to working-class people of my racial group.
  2. Working-class White people and working-class people from my racial group share a common destiny
  3. What happens to working-class Black people in this country will have something to do with what happens to working-class people of my racial group.
  4. Working-class Black people and working-class people from my racial group share a common destiny
  5. What happens to working-class Asian people in this country will have something to do with what happens to working-class people of my racial group.
  6. Working-class Asian people and working-class people from my racial group share a common destiny
  7. What happens to working-class Hispanic people in this country will have something to do with what happens to working-class people of my racial group.
  8. Working-class Hispanic people and working-class people from my racial group share a common destiny

alpha (of all items) = 0.89

df_cbzs_elg %>% 
  ggplot(aes(x = lf)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$lf,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Cross-Race Class Solidarity

Same procedure as the one in the linked fate measure.

  1. I feel a sense of solidarity with working-class White people
  2. I feel a sense of solidarity with working-class Black people
  3. I feel a sense of solidarity with working-class Asian people
  4. I feel a sense of solidarity with working-class Hispanic people

alpha (of all items) = 0.89

df_cbzs_elg %>% 
  ggplot(aes(x = crs)) +
  geom_density(fill = "lightblue",
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(1,7)) +
  ylab("density") +
  geom_vline(xintercept = mean(df_cbzs_elg$crs,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Working-class identification

To what extent do you identify as part of the working class? (1 = Not at all to 7 = Completely)

df_cbzs_elg %>% 
  ggplot(aes(x = class_id)) +
  geom_histogram(fill = "lightblue",
                 binwidth = 1,
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(0,8)) +
  ylab("count") +
  geom_vline(xintercept = mean(df_cbzs_elg$class_id,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Support for redistributive policy

Participants saw one of the following policies and indicated their support for it (1 = Strongly Oppose to 7 = Strongly Support)

Minimum wage increase

Congress has not increased the federal minimum wage, currently set at $7.25, since 2009. Some Congresspeople are proposing a policy that would gradually raise the federal minimum wage to $20 an hour by 2028. After 2028, the minimum wage would be adjusted each year to keep pace with growth in the median wage, a measure of wages for typical workers.

Student debt relief

Some Congresspeople are proposing a policy that would help to address the student loan debt crisis by forgiving up to $50,000 in loans per borrower. Approximately 42 million Americans, or about 1 in 6 American adults, owe a cumulative $1.6 trillion in student loans. Student loans are now the second-largest slice of household debt after mortgages, bigger than credit card debt.

Housing

Some Congresspeople are proposing a housing affordability policy that would help ensure that every American has a place to live. The policy would allow for smaller, lower cost homes like duplexes, townhouses, and garden apartments to be built and developed, allowing new nonprofit homes and reducing overall housing prices.

Climate change

Some Congresspeople are proposing a Green New Deal bill which would phase out the use of fossil fuels, with the government providing clean energy jobs for people who can’t find employment in the private sector. All jobs would pay at least $20 an hour, and include healthcare benefits and collective bargaining rights.

df_cbzs_elg %>% 
  ggplot(aes(x = support)) +
  geom_histogram(fill = "lightblue",
                 binwidth = 1,
                 color = NA) +
  scale_x_continuous(breaks = seq(1,7,1),
                     limits = c(0,8)) +
  ylab("count") +
  geom_vline(xintercept = mean(df_cbzs_elg$support,na.rm = T),
             color = "black",
             linetype = "dashed",
             size = 1.1) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        panel.background = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_line(color = "grey66"),
        axis.text.y = element_text(color = "black"),
        axis.text.x = element_text(color = "black",
                                   face = "bold"),
        axis.title.x = element_text(color = "black",
                                   face = "bold"))

Correlations

Analysis Plan

Linear Model 1A

Predictor: CZSB

Outcome: Cross-race solidarity

m1 <- lm(crs ~ zs_class,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 3.54 [3.08, 4.00] 15.13 483 < .001 NA
Zs class 0.26 [0.18, 0.35] 5.88 483 < .001 0.067

Linear Model 1B

Predictor: CZSB

Outcome: Cross-race solidarity

Controls: Conservatism

m1 <- lm(crs ~ zs_class + ideo_con,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.04 [3.38, 4.71] 11.98 456 < .001 NA
Zs class 0.22 [0.12, 0.32] 4.35 456 < .001 0.069
Ideo con -0.08 [-0.15, -0.01] -2.10 456 .036 0.010

Linear Model 1C

Predictor: CZSB

Outcome: Cross-race solidarity

Controls: Conservatism + SDO + SJ

m1 <- lm(crs ~ zs_class + ideo_con + SDO + sj,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.03 [4.07, 5.99] 10.29 454 < .001 NA
Zs class 0.13 [0.01, 0.25] 2.21 454 .027 0.075
Ideo con 0.01 [-0.07, 0.09] 0.29 454 .770 0.010
SDO -0.39 [-0.51, -0.27] -6.43 454 < .001 0.083
Sj 0.04 [-0.11, 0.19] 0.56 454 .573 0.001

Linear Model 1D

Predictor: CZSB

Outcome: Cross-race solidarity

Controls: Conservatism + SDO + SJ + Relative Deprivation + ZS Mindset + Class Identification

m1 <- lm(crs ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm + class_id,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 3.30 [1.93, 4.68] 4.73 450 < .001 NA
Zs class 0.05 [-0.07, 0.18] 0.84 450 .404 0.081
Ideo con -0.05 [-0.13, 0.03] -1.23 450 .218 0.011
SDO -0.33 [-0.45, -0.22] -5.63 450 < .001 0.090
Sj 0.12 [-0.03, 0.28] 1.59 450 .112 0.001
Reldep -0.15 [-0.26, -0.04] -2.62 450 .009 0.007
Dis 0.21 [0.05, 0.36] 2.57 450 .011 0.018
Zsm -0.01 [-0.13, 0.12] -0.10 450 .921 0.000
Class id 0.24 [0.15, 0.33] 5.30 450 < .001 0.059

Linear Model 1E

Predictor: CZSB

Outcome: Cross-race solidarity

Controls: Conservatism + SDO + SJ + Relative Deprivation + ZS Mindset + Class Identification + income + education + gender (man) + race (white) + age

m1 <- lm(crs ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm + class_id + income_num + edu_num + man + white + age,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 2.73 [1.13, 4.34] 3.35 417 < .001 NA
Zs class 0.04 [-0.09, 0.17] 0.61 417 .540 0.083
Ideo con -0.06 [-0.14, 0.02] -1.53 417 .128 0.013
SDO -0.34 [-0.46, -0.22] -5.60 417 < .001 0.093
Sj 0.14 [-0.02, 0.30] 1.76 417 .079 0.001
Reldep -0.16 [-0.28, -0.04] -2.61 417 .009 0.007
Dis 0.23 [0.07, 0.39] 2.87 417 .004 0.023
Zsm 0.01 [-0.12, 0.13] 0.08 417 .938 0.000
Class id 0.27 [0.17, 0.36] 5.59 417 < .001 0.073
Income num 0.00 [-0.06, 0.06] 0.05 417 .959 0.000
Edu num 0.06 [-0.14, 0.25] 0.57 417 .567 0.000
Man 0.14 [-0.14, 0.41] 0.97 417 .333 0.001
White 0.34 [0.03, 0.65] 2.16 417 .032 0.011
Age 0.00 [-0.01, 0.01] -0.43 417 .667 0.000

Linear Model 2A

Predictor: CZSB

Outcome: Support for policy

m1 <- lm(support ~ zs_class,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 2.82 [2.28, 3.36] 10.33 483 < .001 NA
Zs class 0.50 [0.39, 0.60] 9.52 483 < .001 0.158

Linear Model 2B

Predictor: CZSB

Outcome: Support for policy

Controls: Conservatism

m1 <- lm(support ~ zs_class + ideo_con,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.76 [4.03, 5.49] 12.74 456 < .001 NA
Zs class 0.33 [0.22, 0.44] 5.99 456 < .001 0.192
Ideo con -0.32 [-0.40, -0.24] -7.91 456 < .001 0.121

Linear Model 2C

Predictor: CZSB

Outcome: Support for policy

Controls: Conservatism + SDO + SJ

m1 <- lm(support ~ zs_class + ideo_con + SDO + sj,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 6.75 [5.74, 7.76] 13.13 454 < .001 NA
Zs class 0.15 [0.03, 0.28] 2.47 454 .014 0.223
Ideo con -0.17 [-0.25, -0.08] -3.93 454 < .001 0.142
SDO -0.60 [-0.72, -0.47] -9.44 454 < .001 0.171
Sj -0.04 [-0.19, 0.12] -0.44 454 .657 0.000

Linear Model 2D

Predictor: CZSB

Outcome: Support for policy

Controls: Conservatism + SDO + SJ + Relative Deprivation + ZS Mindset + Class Identification

m1 <- lm(support ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm + class_id,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.61 [4.12, 7.11] 7.37 450 < .001 NA
Zs class 0.09 [-0.04, 0.23] 1.34 450 .181 0.225
Ideo con -0.18 [-0.26, -0.09] -4.15 450 < .001 0.143
SDO -0.60 [-0.73, -0.47] -9.32 450 < .001 0.173
Sj 0.02 [-0.14, 0.19] 0.27 450 .787 0.000
Reldep 0.03 [-0.09, 0.15] 0.47 450 .639 0.003
Dis 0.13 [-0.04, 0.30] 1.48 450 .139 0.005
Zsm 0.08 [-0.05, 0.22] 1.18 450 .238 0.003
Class id 0.03 [-0.07, 0.13] 0.65 450 .516 0.001

Linear Model 2E

Predictor: CZSB

Outcome: Support for policy

Controls: Conservatism + SDO + SJ + Relative Deprivation + ZS Mindset + Class Identification + income + education + gender (man) + race (white) + age

m1 <- lm(support ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm + class_id + income_num + edu_num + man + white + age,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 6.62 [4.89, 8.34] 7.55 417 < .001 NA
Zs class 0.09 [-0.04, 0.23] 1.34 417 .181 0.238
Ideo con -0.18 [-0.27, -0.10] -4.14 417 < .001 0.153
SDO -0.58 [-0.71, -0.45] -8.86 417 < .001 0.176
Sj 0.08 [-0.09, 0.25] 0.91 417 .364 0.001
Reldep 0.02 [-0.10, 0.15] 0.37 417 .710 0.004
Dis 0.15 [-0.02, 0.32] 1.72 417 .086 0.009
Zsm 0.04 [-0.10, 0.18] 0.61 417 .544 0.003
Class id 0.05 [-0.05, 0.15] 0.94 417 .346 0.001
Income num -0.03 [-0.10, 0.03] -1.00 417 .316 0.003
Edu num -0.03 [-0.24, 0.18] -0.29 417 .772 0.000
Man -0.38 [-0.68, -0.08] -2.52 417 .012 0.007
White -0.33 [-0.66, 0.01] -1.94 417 .054 0.014
Age -0.02 [-0.03, 0.00] -2.61 417 .009 0.016

Mediation Model

Predictor: CZSB

Mediator: Cross-race solidarity

Outcome: Support for policy

# Formula for mediator model: crs ~ zs_class
form.m <- reformulate(c("zs_class"), response = "crs")

# Formula for outcome model: support ~ zs_class + crs
form.y <- reformulate(c("zs_class", "crs"), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class"), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "crs",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0634961 0.0323841 0.1012357 0
ADE (direct) 0.4330727 0.3207684 0.5445446 0
Total Effect 0.4965688 0.3848888 0.6062588 0
Prop. Mediated 0.1278697 0.0649696 0.2093163 0
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["crs"]
b_p      <- summary(y.fit)$coefficients["crs", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "crs",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Exploratory analysis

Cross-race solidairity: removing different controls

No SDO

Predictor: CZSB

Outcome: Cross-race solidarity

Controls: Conservatism + SJ + Relative Deprivation + ZS Mindset + Class Identification + income + education + gender (man) + race (white) + age

m1 <- lm(crs ~ zs_class + ideo_con + sj + reldep + dis + zsm + class_id + income_num + edu_num + man + white + age,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 1.96 [0.32, 3.60] 2.35 418 .019 NA
Zs class 0.10 [-0.03, 0.23] 1.57 418 .117 0.078
Ideo con -0.14 [-0.22, -0.06] -3.42 418 < .001 0.013
Sj 0.08 [-0.08, 0.24] 0.96 418 .339 0.000
Reldep -0.19 [-0.31, -0.06] -2.98 418 .003 0.011
Dis 0.26 [0.10, 0.43] 3.13 418 .002 0.029
Zsm -0.04 [-0.17, 0.09] -0.59 418 .558 0.003
Class id 0.29 [0.19, 0.38] 5.86 418 < .001 0.080
Income num -0.01 [-0.08, 0.05] -0.46 418 .648 0.000
Edu num 0.07 [-0.13, 0.28] 0.71 418 .478 0.001
Man 0.12 [-0.16, 0.41] 0.84 418 .401 0.001
White 0.28 [-0.04, 0.60] 1.74 418 .083 0.007
Age 0.00 [-0.01, 0.01] -0.10 418 .918 0.000

No SDO and class id

Predictor: CZSB

Outcome: Cross-race solidarity

Controls: Conservatism + SJ + Relative Deprivation + ZS Mindset + income + education + gender (man) + race (white) + age

m1 <- lm(crs ~ zs_class + ideo_con + sj + reldep + dis + zsm + income_num + edu_num + man + white + age,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 2.92 [1.25, 4.58] 3.44 419 < .001 NA
Zs class 0.17 [0.03, 0.30] 2.47 419 .014 0.073
Ideo con -0.10 [-0.18, -0.01] -2.33 419 .021 0.012
Sj 0.01 [-0.16, 0.18] 0.13 419 .900 0.000
Reldep -0.16 [-0.29, -0.03] -2.44 419 .015 0.011
Dis 0.29 [0.12, 0.46] 3.33 419 < .001 0.027
Zsm -0.05 [-0.19, 0.09] -0.73 419 .466 0.003
Income num -0.04 [-0.10, 0.03] -1.12 419 .264 0.002
Edu num 0.09 [-0.12, 0.30] 0.83 419 .405 0.001
Man 0.16 [-0.14, 0.46] 1.05 419 .293 0.001
White 0.30 [-0.03, 0.64] 1.81 419 .072 0.008
Age 0.00 [-0.01, 0.01] 0.24 419 .811 0.000

Support for policy: removing different controls

No SDO

Predictor: CZSB

Outcome: Support for policy

Controls: Conservatism + SJ + Relative Deprivation + ZS Mindset + Class Identification + income + education + gender (man) + race (white) + age

m1 <- lm(support ~ zs_class + ideo_con + sj + reldep + dis + zsm + class_id + income_num + edu_num + man + white + age,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.30 [3.45, 7.15] 5.63 418 < .001 NA
Zs class 0.20 [0.06, 0.35] 2.71 418 .007 0.209
Ideo con -0.31 [-0.40, -0.22] -6.82 418 < .001 0.132
Sj -0.03 [-0.21, 0.16] -0.30 418 .761 0.009
Reldep -0.02 [-0.16, 0.12] -0.33 418 .739 0.001
Dis 0.20 [0.01, 0.39] 2.11 418 .035 0.014
Zsm -0.03 [-0.18, 0.12] -0.44 418 .663 0.000
Class id 0.08 [-0.02, 0.19] 1.53 418 .127 0.005
Income num -0.06 [-0.13, 0.01] -1.69 418 .092 0.008
Edu num 0.00 [-0.23, 0.23] -0.03 418 .977 0.000
Man -0.40 [-0.73, -0.08] -2.45 418 .014 0.007
White -0.42 [-0.79, -0.06] -2.31 418 .021 0.017
Age -0.01 [-0.03, 0.00] -1.93 418 .054 0.009

Solidarity as DV

Model 1

m1 <- lm(soli ~ zs_class,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.45 [4.21, 4.68] 37.04 483 < .001 NA
Zs class 0.31 [0.26, 0.35] 13.39 483 < .001 0.271

Model 2

m1 <- lm(soli ~ zs_class + ideo_con,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.84 [4.51, 5.17] 29.00 456 < .001 NA
Zs class 0.28 [0.23, 0.33] 11.24 456 < .001 0.310
Ideo con -0.07 [-0.11, -0.03] -3.88 456 < .001 0.032

Model 3

m1 <- lm(soli ~ zs_class + ideo_con + SDO + sj,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 6.25 [5.81, 6.69] 28.02 454 < .001 NA
Zs class 0.15 [0.10, 0.20] 5.57 454 < .001 0.365
Ideo con 0.02 [-0.02, 0.06] 1.13 454 .259 0.041
SDO -0.27 [-0.32, -0.21] -9.77 454 < .001 0.199
Sj -0.13 [-0.20, -0.06] -3.70 454 < .001 0.029

Model 4

m1 <- lm(soli ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm + class_id,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.61 [4.01, 5.21] 15.10 450 < .001 NA
Zs class 0.09 [0.03, 0.14] 3.24 450 .001 0.405
Ideo con -0.02 [-0.05, 0.02] -0.95 450 .342 0.048
SDO -0.24 [-0.29, -0.19] -9.18 450 < .001 0.227
Sj -0.05 [-0.11, 0.02] -1.35 450 .177 0.035
Reldep -0.03 [-0.08, 0.02] -1.13 450 .258 0.000
Dis 0.19 [0.12, 0.26] 5.49 450 < .001 0.073
Zsm -0.01 [-0.07, 0.04] -0.50 450 .618 0.001
Class id 0.14 [0.10, 0.18] 6.89 450 < .001 0.095

Model 5

m1 <- lm(soli ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm + class_id + income_num + edu_num + man + white + age,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.16 [3.46, 4.87] 11.63 417 < .001 NA
Zs class 0.08 [0.02, 0.13] 2.78 417 .006 0.406
Ideo con -0.01 [-0.05, 0.02] -0.62 417 .534 0.054
SDO -0.25 [-0.30, -0.20] -9.34 417 < .001 0.238
Sj -0.07 [-0.14, 0.00] -1.98 417 .049 0.040
Reldep -0.01 [-0.06, 0.05] -0.26 417 .797 0.000
Dis 0.20 [0.13, 0.28] 5.74 417 < .001 0.074
Zsm -0.02 [-0.07, 0.04] -0.55 417 .585 0.001
Class id 0.14 [0.10, 0.19] 6.95 417 < .001 0.099
Income num 0.04 [0.01, 0.07] 2.97 417 .003 0.024
Edu num 0.06 [-0.02, 0.15] 1.48 417 .140 0.006
Man 0.12 [0.00, 0.24] 1.95 417 .052 0.009
White -0.04 [-0.17, 0.10] -0.51 417 .608 0.000
Age 0.00 [0.00, 0.01] 0.47 417 .638 0.001

Linked Fate as DV

Model 1

m1 <- lm(lf ~ zs_class,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 3.88 [3.47, 4.29] 18.72 483 < .001 NA
Zs class 0.14 [0.07, 0.22] 3.66 483 < .001 0.027

Model 2

m1 <- lm(lf ~ zs_class + ideo_con,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.61 [4.03, 5.19] 15.53 456 < .001 NA
Zs class 0.07 [-0.01, 0.16] 1.68 456 .094 0.024
Ideo con -0.10 [-0.16, -0.04] -3.06 456 .002 0.020

Model 3

m1 <- lm(lf ~ zs_class + ideo_con + SDO + sj,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 5.29 [4.44, 6.14] 12.21 454 < .001 NA
Zs class 0.01 [-0.09, 0.12] 0.26 454 .791 0.025
Ideo con -0.03 [-0.10, 0.04] -0.90 454 .369 0.022
SDO -0.31 [-0.42, -0.21] -5.84 454 < .001 0.069
Sj 0.06 [-0.07, 0.19] 0.89 454 .376 0.002

Model 4

m1 <- lm(lf ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm + class_id,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 4.10 [2.85, 5.35] 6.45 450 < .001 NA
Zs class -0.04 [-0.15, 0.08] -0.63 450 .531 0.026
Ideo con -0.06 [-0.14, 0.01] -1.79 450 .074 0.022
SDO -0.28 [-0.39, -0.18] -5.26 450 < .001 0.071
Sj 0.12 [-0.02, 0.26] 1.65 450 .099 0.002
Reldep -0.06 [-0.17, 0.04] -1.20 450 .230 0.001
Dis 0.15 [0.01, 0.29] 2.05 450 .041 0.011
Zsm 0.00 [-0.12, 0.11] -0.07 450 .948 0.000
Class id 0.12 [0.04, 0.20] 2.94 450 .003 0.019

Model 5

m1 <- lm(lf ~ zs_class + ideo_con + SDO + sj + reldep + dis + zsm  + class_id + income_num + edu_num + man + white + age,data = df_cbzs_elg)

eta_table <- eta_squared(m1)
etas_for_table <- c(NA,eta_table$Eta2_partial)
apa_lm <- apa_print(m1)
table_for_print <- apa_lm$table %>% 
  mutate(eta2 = round(etas_for_table,3))
 
kbl(table_for_print) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
term estimate conf.int statistic df p.value eta2
Intercept 3.17 [1.68, 4.65] 4.20 417 < .001 NA
Zs class -0.04 [-0.16, 0.08] -0.71 417 .479 0.024
Ideo con -0.06 [-0.14, 0.01] -1.61 417 .108 0.022
SDO -0.28 [-0.39, -0.17] -4.99 417 < .001 0.068
Sj 0.11 [-0.04, 0.26] 1.47 417 .142 0.002
Reldep -0.04 [-0.16, 0.07] -0.79 417 .432 0.001
Dis 0.18 [0.03, 0.33] 2.36 417 .019 0.014
Zsm 0.00 [-0.12, 0.12] 0.01 417 .988 0.000
Class id 0.12 [0.04, 0.21] 2.84 417 .005 0.020
Income num 0.02 [-0.04, 0.07] 0.64 417 .522 0.002
Edu num 0.20 [0.01, 0.38] 2.12 417 .035 0.010
Man 0.11 [-0.14, 0.37] 0.89 417 .376 0.001
White 0.14 [-0.15, 0.42] 0.93 417 .353 0.002
Age 0.00 [-0.01, 0.01] -0.09 417 .927 0.000

Mediation models

Model 1A

Predictor: CBZS

Mediator: Solidarity

Outcome: Support for Policy

Controls: Conservatism

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con")

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.1563137 0.0937594 0.2247176 0.0000
ADE (direct) 0.1784169 0.0457457 0.3124222 0.0076
Total Effect 0.3347306 0.2126204 0.4558373 0.0000
Prop. Mediated 0.4669835 0.2611169 0.8007835 0.0000
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 1B

Predictor: CBZS

Mediator: Solidarity

Outcome: Support for Policy

Controls: SDO

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "SDO")

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0420931 0.0060804 0.0829549 0.0242
ADE (direct) 0.1704020 0.0587367 0.2851049 0.0032
Total Effect 0.2124951 0.1048791 0.3241451 0.0002
Prop. Mediated 0.1980900 0.0285397 0.5082581 0.0244
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 1C

Predictor: CBZS

Mediator: Solidarity

Outcome: Support for Policy

Controls: Conservatism, SDO, system justification

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj"
)

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0275091 -0.0051033 0.0666504 0.1004
ADE (direct) 0.1260965 -0.0072015 0.2563454 0.0666
Total Effect 0.1536055 0.0199395 0.2803902 0.0210
Prop. Mediated 0.1790891 -0.0615158 0.9632805 0.1174
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 1D

Predictor: CBZS

Mediator: Solidarity

Outcome: Support for Policy

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm"
)

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0177429 -0.0069849 0.0465728 0.1680
ADE (direct) 0.0805585 -0.0667959 0.2257158 0.2694
Total Effect 0.0983014 -0.0479556 0.2397367 0.1874
Prop. Mediated 0.1804945 -1.2193450 1.4790343 0.3198
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 1E

Predictor: CBZS

Mediator: Solidarity

Outcome: Support for Policy

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset, income, education, gender (man = 1, non-man = 0), race (white = 1, non-white = 0), age

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm",
  "income_num", "edu_num", "man", "white", "age"
)

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0248685 -0.0000694 0.0549369 0.0518
ADE (direct) 0.0776186 -0.0674530 0.2218885 0.2772
Total Effect 0.1024871 -0.0444314 0.2443337 0.1632
Prop. Mediated 0.2426498 -1.3021745 1.9746837 0.2038
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 2A

Predictor: CBZS

Mediator: Cross-Race Solidarity

Outcome: Support for Policy

Controls: Conservatism

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con")

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.1563137 0.0937195 0.2249590 0.0000
ADE (direct) 0.1784169 0.0502665 0.3107961 0.0072
Total Effect 0.3347306 0.2154460 0.4566040 0.0000
Prop. Mediated 0.4669835 0.2619011 0.7898175 0.0000
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 2B

Predictor: CBZS

Mediator: Cross-Race Solidarity

Outcome: Support for Policy

Controls: SDO

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "SDO")

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0420931 0.0059205 0.0825147 0.024
ADE (direct) 0.1704020 0.0561031 0.2826611 0.002
Total Effect 0.2124951 0.1004165 0.3202898 0.000
Prop. Mediated 0.1980900 0.0286443 0.5107630 0.024
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 2C

Predictor: CBZS

Mediator: Cross-Race Solidarity

Outcome: Support for Policy

Controls: Conservatism, SDO, system justification

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj"
)

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0275091 -0.0051975 0.0660124 0.1004
ADE (direct) 0.1260965 -0.0059906 0.2566801 0.0618
Total Effect 0.1536055 0.0223097 0.2791634 0.0200
Prop. Mediated 0.1790891 -0.0551471 0.9028167 0.1172
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 2D

Predictor: CBZS

Mediator: Cross-Race Solidarity

Outcome: Support for Policy

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm"
)

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "soli")

# Formula for outcome model: support ~ zs_class + soli + controls
form.y <- reformulate(c("zs_class", "soli", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "soli",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0177429 -0.0075471 0.0464710 0.1760
ADE (direct) 0.0805585 -0.0669354 0.2272581 0.2904
Total Effect 0.0983014 -0.0486295 0.2409273 0.1982
Prop. Mediated 0.1804945 -1.3071185 1.9276828 0.3346
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["soli"]
b_p      <- summary(y.fit)$coefficients["soli", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "soli",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 2E

Predictor: CBZS

Mediator: Cross-Race Solidarity

Outcome: Support for Policy

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset, income, education, gender (man = 1, non-man = 0), race (white = 1, non-white = 0), age

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm",
  "income_num", "edu_num", "man", "white", "age"
)

# Formula for mediator model: crs ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "crs")

# Formula for outcome model: support ~ zs_class + crs + controls
form.y <- reformulate(c("zs_class", "crs", controls), response = "support")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "support"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "crs",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0053397 -0.0059783 0.0213463 0.4262
ADE (direct) 0.0971474 -0.0514101 0.2390275 0.1916
Total Effect 0.1024871 -0.0468877 0.2439746 0.1690
Prop. Mediated 0.0521015 -0.3412837 0.5715187 0.5240
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["crs"]
b_p      <- summary(y.fit)$coefficients["crs", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "crs",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "support",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 3A

Predictor: CBZS

Mediator: Linked Fate

Outcome: Cross-Race Solidarity

Controls: Conservatism

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con")

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: crs ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "crs"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0463222 -0.0149871 0.1165159 0.1448
ADE (direct) 0.1733322 0.0836684 0.2617844 0.0000
Total Effect 0.2196544 0.1179295 0.3204910 0.0000
Prop. Mediated 0.2108866 -0.0908024 0.4847934 0.1448
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 3B

Predictor: CBZS

Mediator: Linked Fate

Outcome: Cross-Race Solidarity

Controls: SDO

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "SDO")

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: crs ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "crs"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0130783 -0.0431376 0.0693075 0.6676
ADE (direct) 0.0936936 0.0076136 0.1855288 0.0316
Total Effect 0.1067720 0.0128285 0.2064752 0.0280
Prop. Mediated 0.1224885 -0.9949817 0.8141800 0.6508
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 3C

Predictor: CBZS

Mediator: Linked Fate

Outcome: Cross-Race Solidarity

Controls: Conservatism, SDO, system justification

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj"
)

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: crs ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "crs"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0079879 -0.0567091 0.0779260 0.8218
ADE (direct) 0.1229664 0.0173754 0.2295952 0.0238
Total Effect 0.1309543 0.0075780 0.2566587 0.0352
Prop. Mediated 0.0609977 -1.3560420 0.6846491 0.7942
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 3D

Predictor: CBZS

Mediator: Linked Fate

Outcome: Cross-Race Solidarity

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm"
)

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: crs ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "crs"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) -0.0065343 -0.0781814 0.0675599 0.8402
ADE (direct) 0.1069192 -0.0088980 0.2216276 0.0696
Total Effect 0.1003849 -0.0358408 0.2380738 0.1484
Prop. Mediated -0.0650926 -3.4450263 2.6454081 0.9666
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 3E

Predictor: CBZS

Mediator: Linked Fate

Outcome: Cross-Race Solidarity

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset, income, education, gender (man = 1, non-man = 0), race (white = 1, non-white = 0), age

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm",
  "income_num", "edu_num", "man", "white", "age"
)

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: crs ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "crs")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "crs"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) -0.0093592 -0.0792584 0.0604835 0.7754
ADE (direct) 0.1018403 -0.0149108 0.2188898 0.0912
Total Effect 0.0924810 -0.0445672 0.2251353 0.1914
Prop. Mediated -0.1012018 -3.8617309 3.2776628 0.9388
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "crs",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 4A

Predictor: CBZS

Mediator: Linked Fate

Outcome: Solidarity

Controls: Conservatism

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con")

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: soli ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0137371 -0.0048637 0.0353257 0.1494
ADE (direct) 0.2667962 0.2164397 0.3174520 0.0000
Total Effect 0.2805333 0.2293350 0.3345103 0.0000
Prop. Mediated 0.0489677 -0.0185141 0.1218977 0.1494
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 4B

Predictor: CBZS

Mediator: Linked Fate

Outcome: Solidarity

Controls: SDO

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "SDO")

# Formula for mediator model: soli ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: soli ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0028717 -0.0098790 0.0152427 0.6602
ADE (direct) 0.1767823 0.1298862 0.2237666 0.0000
Total Effect 0.1796540 0.1338276 0.2263882 0.0000
Prop. Mediated 0.0159848 -0.0569513 0.0861946 0.6602
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 4C

Predictor: CBZS

Mediator: Linked Fate

Outcome: Solidarity

Controls: Conservatism, SDO, system justification

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj"
)

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: soli ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) 0.0018393 -0.0132841 0.0190698 0.8084
ADE (direct) 0.1487922 0.0926254 0.2013833 0.0000
Total Effect 0.1506315 0.0929201 0.2058700 0.0000
Prop. Mediated 0.0122106 -0.1037245 0.1231341 0.8084
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 4D

Predictor: CBZS

Mediator: Linked Fate

Outcome: Solidarity

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm"
)

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: soli ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) -0.0013929 -0.0164728 0.0158841 0.8478
ADE (direct) 0.1176437 0.0605541 0.1736988 0.0002
Total Effect 0.1162507 0.0566392 0.1766186 0.0004
Prop. Mediated -0.0119823 -0.2009965 0.1229879 0.8482
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Model 4E

Predictor: CBZS

Mediator: Linked Fate

Outcome: Solidarity

Controls: Conservatism, SDO, system justification, relative deprivation, working-class disadvantage, zero-sum mindset, income, education, gender (man = 1, non-man = 0), race (white = 1, non-white = 0), age

Bootstraps: 10,000

# Vector of control variable names
controls <- c(
  "ideo_con", "SDO", "sj", "reldep", "dis", "zsm",
  "income_num", "edu_num", "man", "white", "age"
)

# Formula for mediator model: lf ~ zs_class + controls
form.m <- reformulate(c("zs_class", controls), response = "lf")

# Formula for outcome model: soli ~ zs_class + lf + controls
form.y <- reformulate(c("zs_class", "lf", controls), response = "soli")

# Fit linear models
m.fit <- lm(form.m, data = df_cbzs_elg)        # a-path
y.fit <- lm(form.y, data = df_cbzs_elg)        # b and c'-paths

# Fit outcome model WITHOUT mediator to get c-path (total effect)
y.fit.total <- lm(
  reformulate(c("zs_class", controls), response = "soli"),
  data = df_cbzs_elg
)

# Mediation analysis with bootstrapping (10,000 sims)
med.fit <- mediation::mediate(
  model.m   = m.fit,
  model.y   = y.fit,
  treat     = "zs_class",
  mediator  = "lf",
  boot      = TRUE,
  sims      = 10000
)

med_tbl <- tibble(
  Effect   = c("ACME (indirect)", "ADE (direct)",
               "Total Effect", "Prop. Mediated"),
  Estimate = c(med.fit$d0,        med.fit$z0,
               med.fit$tau.coef,  med.fit$n0),
  CI.lower = c(med.fit$d0.ci[1],  med.fit$z0.ci[1],
               med.fit$tau.ci[1], med.fit$n0.ci[1]),
  CI.upper = c(med.fit$d0.ci[2],  med.fit$z0.ci[2],
               med.fit$tau.ci[2], med.fit$n0.ci[2]),
  p.value  = c(med.fit$d0.p,      med.fit$z0.p,
               med.fit$tau.p,     med.fit$n0.p)
)

kbl(med_tbl) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
ACME (indirect) -0.0020054 -0.0172462 0.0146144 0.7756
ADE (direct) 0.1095667 0.0491073 0.1681990 0.0002
Total Effect 0.1075613 0.0448384 0.1692587 0.0004
Prop. Mediated -0.0186444 -0.2344983 0.1270720 0.7760
# Helper to generate significance stars
p_stars <- function(p) {
  if (p < .001) return("***")
  if (p < .01)  return("**")
  if (p < .05)  return("*")
  return("")
}

# Extract coefficients & p-values
a_coef   <- coef(m.fit)["zs_class"]
a_p      <- summary(m.fit)$coefficients["zs_class", "Pr(>|t|)"]

b_coef   <- coef(y.fit)["lf"]
b_p      <- summary(y.fit)$coefficients["lf", "Pr(>|t|)"]

cprime   <- coef(y.fit)["zs_class"]
cprime_p <- summary(y.fit)$coefficients["zs_class", "Pr(>|t|)"]

c_total  <- coef(y.fit.total)["zs_class"]
c_total_p <- summary(y.fit.total)$coefficients["zs_class", "Pr(>|t|)"]

# Paste coefficient + stars
a_label      <- paste0("a = ", round(a_coef, 3), p_stars(a_p))
b_label      <- paste0("b = ", round(b_coef, 3), p_stars(b_p))
cprime_label <- paste0("c' = ", round(cprime, 3), p_stars(cprime_p))
c_label      <- paste0("c = ", round(c_total, 3), p_stars(c_total_p))

# Plot
ggplot() +
  xlim(0, 3) + ylim(0, 2) +

  # Nodes
  annotate("text", x = 0.5, y = 1,   label = "zs_class", fontface = "bold") +
  annotate("text", x = 1.5, y = 1,   label = "lf",     fontface = "bold") +
  annotate("text", x = 2.5, y = 1,   label = "soli",  fontface = "bold") +

  # a-path (X → M)
  annotate("segment",
           x = 0.7, xend = 1.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.0, y = 1.15, label = a_label) +

  # b-path (M → Y)
  annotate("segment",
           x = 1.7, xend = 2.3, y = 1, yend = 1,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 2.0, y = 1.15, label = b_label) +

  # c'-path (direct effect)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 0.9, yend = 0.9,
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 0.75, label = cprime_label) +

  # c-path (total effect, dashed)
  annotate("segment",
           x = 0.5, xend = 2.5, y = 1.1, yend = 1.1,
           linetype = "dashed",
           arrow = arrow(length = unit(0.20, "cm"))) +
  annotate("text", x = 1.5, y = 1.25, label = c_label) +

  theme_void()

Structural Equation Models

Model 1

library(lavaan)
## This is lavaan 0.6-17
## lavaan is FREE software! Please report any bugs.
## 
## Attaching package: 'lavaan'
## The following object is masked from 'package:psych':
## 
##     cor2cov
model_chain <- '
  # Regressions (paths)
  lf      ~ a * zs_class
  crs     ~ b * lf
  support ~ c * crs + cprime * zs_class   # include direct path zs_class → support

  # Indirect effects
  ind_zs_lf_crs      := a * b           # zs_class → lf → crs
  ind_zs_lf_crs_sup  := a * b * c       # zs_class → lf → crs → support (full chain)

  # Total indirect effect to support (here only the chain)
  ind_total          := a * b * c

  # Total effect of zs_class on support
  total              := cprime + ind_total
'

fit_chain <- sem(
  model_chain,
  data      = df_cbzs_elg,
  se        = "bootstrap",   # bootstrap standard errors
  bootstrap = 10000          # number of bootstrap draws
)

# extract parameter estimates & bootstrap CIs
pe <- parameterEstimates(fit_chain, boot.ci.type = "perc")

# filter for the effects we defined via :=
effects <- pe %>%
  filter(label %in% c(
    "ind_zs_lf_crs",
    "ind_zs_lf_crs_sup",
    "ind_total",
    "cprime",
    "total"
  )) %>%
  transmute(
    Effect = c(
      "Indirect (zs_class → lf → crs)",
      "Indirect (zs_class → lf → crs → support)",
      "Total Indirect",
      "Direct (zs_class → support)",
      "Total Effect"
    ),
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
Indirect (zs_class → lf → crs) 0.4330727 0.3209626 0.5435635 0.0000000
Indirect (zs_class → lf → crs → support) 0.0919424 0.0328834 0.1566833 0.0034244
Total Indirect 0.0221911 0.0069966 0.0423987 0.0143210
Direct (zs_class → support) 0.0221911 0.0069966 0.0423987 0.0143210
Total Effect 0.4552638 0.3441300 0.5651008 0.0000000
library(semPlot)

semPaths(
  fit_chain,
  whatLabels = "std",    # standardized estimates
  layout     = "circle", # or "tree"
  edge.label.cex = 1.0,
  sizeMan    = 7
)

Model 2

library(lavaan)

model_chain <- '
  # Regressions (paths)
  lf      ~ a * zs_class
  soli     ~ b * lf
  support ~ c * soli + cprime * zs_class   # include direct path zs_class → support

  # Indirect effects
  ind_zs_lf_soli      := a * b           # zs_class → lf → soli
  ind_zs_lf_soli_sup  := a * b * c       # zs_class → lf → soli → support (full chain)

  # Total indirect effect to support (here only the chain)
  ind_total          := a * b * c

  # Total effect of zs_class on support
  total              := cprime + ind_total
'

fit_chain <- sem(
  model_chain,
  data      = df_cbzs_elg,
  se        = "bootstrap",   # bootstrap standard errors
  bootstrap = 10000          # number of bootstrap draws
)

# extract parameter estimates & bootstrap CIs
pe <- parameterEstimates(fit_chain, boot.ci.type = "perc")

# filter for the effects we defined via :=
effects <- pe %>%
  filter(label %in% c(
    "ind_zs_lf_soli",
    "ind_zs_lf_soli_sup",
    "ind_total",
    "cprime",
    "total"
  )) %>%
  transmute(
    Effect = c(
      "Indirect (zs_class → lf → soli)",
      "Indirect (zs_class → lf → soli → support)",
      "Total Indirect",
      "Direct (zs_class → support)",
      "Total Effect"
    ),
    Estimate = est,
    CI.lower = ci.lower,
    CI.upper = ci.upper,
    p.value  = pvalue
  )

kbl(effects) %>% 
  kable_styling(bootstrap_options = "hover",
                full_width = F,
                position = "left")
Effect Estimate CI.lower CI.upper p.value
Indirect (zs_class → lf → soli) 0.2778823 0.1502391 0.4073872 0.0000174
Indirect (zs_class → lf → soli → support) 0.0347631 0.0103844 0.0662830 0.0148642
Total Indirect 0.0247316 0.0073229 0.0485433 0.0205780
Direct (zs_class → support) 0.0247316 0.0073229 0.0485433 0.0205780
Total Effect 0.3026139 0.1739205 0.4300465 0.0000026
library(semPlot)

semPaths(
  fit_chain,
  whatLabels = "std",    # standardized estimates
  layout     = "circle", # or "tree"
  edge.label.cex = 1.0,
  sizeMan    = 7
)

Parallel Mediations

Model 1

Predictor: CBZS

Mediators: (1) Solidarity; (2) Linked fate

Outcome: Support for policy

Bootstraps: 10,000

label est se ci.lower ci.upper pvalue
a1 0.3073884 0.0263705 0.2568415 0.3615591 0.0000000
a2 0.1448392 0.0467876 0.0527885 0.2371838 0.0019637
b1 0.6784793 0.1017358 0.4756103 0.8720381 0.0000000
b2 0.0588250 0.0638900 -0.0647456 0.1850704 0.3571956
c_prime 0.2794920 0.0647669 0.1555012 0.4061759 0.0000159
ind1 0.2085566 0.0352774 0.1419476 0.2796825 0.0000000
ind2 0.0085202 0.0097563 -0.0104668 0.0291929 0.3825019
ind_total 0.2170768 0.0333411 0.1532169 0.2828670 0.0000000
total 0.4965688 0.0560477 0.3879517 0.6083244 0.0000000

Model 2

Predictor: SDO

Mediators: (1) Solidarity; (2) Linked fate

Outcome: Support for policy

Bootstraps: 10,000

label est se ci.lower ci.upper pvalue
a1 -0.3969026 0.0306146 -0.4592265 -0.3393344 0.0000000
a2 -0.3019661 0.0442531 -0.3874100 -0.2145822 0.0000000
b1 0.3528505 0.1039296 0.1579783 0.5646963 0.0006861
b2 -0.0203140 0.0543516 -0.1268793 0.0863134 0.7085891
c_prime -0.6541969 0.0784168 -0.8049942 -0.4972563 0.0000000
ind1 -0.1400473 0.0434084 -0.2313724 -0.0615411 0.0012542
ind2 0.0061341 0.0164840 -0.0273726 0.0382178 0.7097990
ind_total -0.1339132 0.0438205 -0.2244318 -0.0538400 0.0022435
total -0.7881100 0.0551292 -0.8958551 -0.6782111 0.0000000